├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── pre-commit_autoupdate.yml │ ├── prepare_release.yml │ ├── pypi.yml │ ├── scorecard.yml │ └── tag_release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS.rst ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── examples ├── caching_requestor.py ├── device_id_auth_trophies.py ├── obtain_refresh_token.py ├── read_only_auth_trophies.py └── script_auth_friend_list.py ├── prawcore ├── __init__.py ├── auth.py ├── const.py ├── exceptions.py ├── rate_limit.py ├── requestor.py ├── sessions.py └── util.py ├── pyproject.toml ├── tests ├── __init__.py ├── conftest.py ├── integration │ ├── __init__.py │ ├── cassettes │ │ ├── TestAuthorizer.test_authorize__with_invalid_code.json │ │ ├── TestAuthorizer.test_authorize__with_permanent_grant.json │ │ ├── TestAuthorizer.test_authorize__with_temporary_grant.json │ │ ├── TestAuthorizer.test_refresh.json │ │ ├── TestAuthorizer.test_refresh__with_invalid_token.json │ │ ├── TestAuthorizer.test_revoke__access_token_with_refresh_set.json │ │ ├── TestAuthorizer.test_revoke__access_token_without_refresh_set.json │ │ ├── TestAuthorizer.test_revoke__refresh_token_with_access_set.json │ │ ├── TestAuthorizer.test_revoke__refresh_token_without_access_set.json │ │ ├── TestDeviceIDAuthorizer.test_refresh.json │ │ ├── TestDeviceIDAuthorizer.test_refresh__with_scopes_and_trusted_authenticator.json │ │ ├── TestDeviceIDAuthorizer.test_refresh__with_short_device_id.json │ │ ├── TestReadOnlyAuthorizer.test_refresh.json │ │ ├── TestReadOnlyAuthorizer.test_refresh__with_scopes.json │ │ ├── TestScriptAuthorizer.test_refresh.json │ │ ├── TestScriptAuthorizer.test_refresh__with_invalid_otp.json │ │ ├── TestScriptAuthorizer.test_refresh__with_invalid_username_or_password.json │ │ ├── TestScriptAuthorizer.test_refresh__with_scopes.json │ │ ├── TestScriptAuthorizer.test_refresh__with_valid_otp.json │ │ ├── TestSession.test_request__accepted.json │ │ ├── TestSession.test_request__bad_gateway.json │ │ ├── TestSession.test_request__bad_json.json │ │ ├── TestSession.test_request__bad_request.json │ │ ├── TestSession.test_request__cloudflare_connection_timed_out.json │ │ ├── TestSession.test_request__cloudflare_unknown_error.json │ │ ├── TestSession.test_request__conflict.json │ │ ├── TestSession.test_request__created.json │ │ ├── TestSession.test_request__forbidden.json │ │ ├── TestSession.test_request__gateway_timeout.json │ │ ├── TestSession.test_request__get.json │ │ ├── TestSession.test_request__internal_server_error.json │ │ ├── TestSession.test_request__no_content.json │ │ ├── TestSession.test_request__not_found.json │ │ ├── TestSession.test_request__okay_with_0_byte_content.json │ │ ├── TestSession.test_request__patch.json │ │ ├── TestSession.test_request__post.json │ │ ├── TestSession.test_request__post__with_files.json │ │ ├── TestSession.test_request__raw_json.json │ │ ├── TestSession.test_request__redirect.json │ │ ├── TestSession.test_request__redirect_301.json │ │ ├── TestSession.test_request__service_unavailable.json │ │ ├── TestSession.test_request__too__many_requests__with_retry_headers.json │ │ ├── TestSession.test_request__too__many_requests__without_retry_headers.json │ │ ├── TestSession.test_request__too_large.json │ │ ├── TestSession.test_request__unavailable_for_legal_reasons.json │ │ ├── TestSession.test_request__unexpected_status_code.json │ │ ├── TestSession.test_request__unsupported_media_type.json │ │ ├── TestSession.test_request__uri_too_long.json │ │ ├── TestSession.test_request__with_insufficient_scope.json │ │ ├── TestSession.test_request__with_invalid_access_token.json │ │ ├── TestSession.test_request__with_invalid_access_token__retry.json │ │ ├── TestTrustedAuthenticator.test_revoke_token.json │ │ ├── TestTrustedAuthenticator.test_revoke_token__with_access_token_hint.json │ │ ├── TestTrustedAuthenticator.test_revoke_token__with_refresh_token_hint.json │ │ └── TestUntrustedAuthenticator.test_revoke_token.json │ ├── files │ │ ├── comment_ids.txt │ │ ├── too_large.jpg │ │ └── white-square.png │ ├── test_authenticator.py │ ├── test_authorizer.py │ └── test_sessions.py ├── unit │ ├── __init__.py │ ├── test_authenticator.py │ ├── test_authorizer.py │ ├── test_rate_limit.py │ ├── test_requestor.py │ └── test_sessions.py └── utils.py └── uv.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | @abstract 4 | if TYPE_CHECKING: 5 | pragma: no cover 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - id: bug-description 3 | attributes: 4 | label: Describe the Bug 5 | placeholder: A clear and concise description of what the bug is. 6 | type: textarea 7 | validations: 8 | required: true 9 | - id: desired-result 10 | attributes: 11 | label: Desired Result 12 | placeholder: Describe the desired result. 13 | type: textarea 14 | validations: 15 | required: true 16 | - id: code 17 | attributes: 18 | description: | 19 | Provide your [Minimal, Complete, and Verifiable](https://stackoverflow.com/help/mcve) 20 | code example here, without the Reddit() initialization, to not leak private credentials. 21 | This will be automatically formatted into code, so no need for backticks. 22 | label: Code to reproduce the bug 23 | placeholder: Insert code here... 24 | render: Python 25 | type: textarea 26 | validations: 27 | required: true 28 | - id: credential-check 29 | attributes: 30 | description: | 31 | The `Reddit()` initialization in my code example does not include the following parameters to prevent credential leakage: 32 | `client_secret`, `password`, or `refresh_token`. 33 | label: My code does not include sensitive credentials 34 | options: 35 | - label: "Yes, I have removed sensitive credentials from my code." 36 | required: true 37 | type: checkboxes 38 | - id: logs 39 | attributes: 40 | description: | 41 | Please copy and paste any relevant log output. 42 | This will be automatically formatted into code, so no need for backticks. 43 | label: Relevant Logs 44 | render: Shell 45 | type: textarea 46 | validations: 47 | required: true 48 | - id: previously-worked 49 | attributes: 50 | label: This code has previously worked as intended 51 | multiple: false 52 | options: 53 | - "I'm not sure, I haven't used this code before." 54 | - "Yes" 55 | - "No" 56 | type: dropdown 57 | validations: 58 | required: true 59 | - id: environment 60 | attributes: 61 | description: What operating system, version, and/or environment are you working with? 62 | label: Operating System/Environment 63 | placeholder: "Example: macOS Sonoma 14.1.1" 64 | type: input 65 | validations: 66 | required: true 67 | - id: python-version 68 | attributes: 69 | description: | 70 | What implementation and version of Python are you working with? 71 | CPython is assumed unless indicated otherwise. 72 | label: Python Version 73 | placeholder: "Example: 3.12.0" 74 | type: input 75 | validations: 76 | required: true 77 | - id: prawcore-version 78 | attributes: 79 | description: What version of `prawcore` are you encountering this issue with? Obtain this by running `pip show prawcore`. 80 | label: prawcore Version 81 | type: input 82 | validations: 83 | required: true 84 | - id: anything-else 85 | attributes: 86 | description: Anything that will give us more context about the issue you are encountering! 87 | label: Links, references, and/or additional comments? 88 | type: textarea 89 | description: File a bug report 90 | labels: [ "bug", "unverified" ] 91 | name: Bug Report 92 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: r/redditdev subreddit 4 | about: For general help using PRAW, please ask here. 5 | url: https://redditdev.reddit.com 6 | - name: Official praw-dev Slack 7 | about: For more real-time help, feel free to join our Slack. 8 | url: https://join.slack.com/t/praw/shared_invite/enQtOTUwMDcxOTQ0NzY5LWVkMGQ3ZDk5YmQ5MDEwYTZmMmJkMTJkNjBkNTY3OTU0Y2E2NGRlY2ZhZTAzMWZmMWRiMTMwYjdjODkxOGYyZjY 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: textarea 3 | attributes: 4 | label: Describe the solution you'd like 5 | placeholder: A clear and concise description of what you want to happen. 6 | id: feature-description 7 | validations: 8 | required: true 9 | - type: textarea 10 | attributes: 11 | label: Describe alternatives you've considered 12 | placeholder: Clear and concise description of any alternative solutions or features you've considered. 13 | id: alternatives-considered 14 | - type: textarea 15 | attributes: 16 | label: Additional context 17 | placeholder: Add any other context or links here. 18 | id: additional-context 19 | description: Suggest an idea for this project 20 | labels: ["Feature Request"] 21 | name: Feature Request 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: pip 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | concurrency: 2 | group: check-${{ github.ref }} 3 | cancel-in-progress: true 4 | jobs: 5 | test: 6 | name: test with ${{ matrix.env }} on ${{ matrix.os }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - env: "3.9" 13 | os: macos-latest 14 | - env: "3.9" 15 | os: ubuntu-latest 16 | - env: "3.9" 17 | os: windows-latest 18 | - env: "3.10" 19 | os: ubuntu-latest 20 | - env: "3.11" 21 | os: ubuntu-latest 22 | - env: "3.12" 23 | os: ubuntu-latest 24 | - env: "3.13" 25 | os: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Install the latest version of uv 29 | uses: astral-sh/setup-uv@v6 30 | - name: Install tox 31 | run: uv tool install --python-preference only-managed --python ${{ matrix.env }} tox --with tox-uv 32 | - name: Run test suite 33 | run: | 34 | tox run --skip-pkg-install -e ${{ fromJson('{ "3.9": "py39", "3.10": "py310", "3.11": "py311", "3.12": "py312", "3.13": "py313", }')[matrix.env] }} 35 | - name: Run lint tests 36 | if: matrix.env == '3.9' 37 | run: tox run --skip-pkg-install --skip-env py 38 | 39 | name: CI 40 | on: 41 | workflow_dispatch: 42 | push: 43 | branches: ["main"] 44 | pull_request: 45 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit_autoupdate.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | pre-commit_autoupdate: 3 | name: Update pre-commit hooks 4 | secrets: inherit 5 | uses: praw-dev/.github/.github/workflows/pre-commit_autoupdate.yml@main 6 | name: Update pre-commit hooks 7 | on: 8 | schedule: 9 | - cron: 0 15 * * 1 10 | workflow_dispatch: 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | -------------------------------------------------------------------------------- /.github/workflows/prepare_release.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | prepare_release: 3 | name: Prepare Release 4 | secrets: inherit 5 | uses: praw-dev/.github/.github/workflows/prepare_release.yml@main 6 | with: 7 | package: prawcore 8 | version: ${{ inputs.version }} 9 | version_file: __init__.py 10 | name: Prepare Release 11 | on: 12 | workflow_dispatch: 13 | inputs: 14 | version: 15 | description: The version to prepare for release 16 | required: true 17 | permissions: 18 | contents: read 19 | pull-requests: write 20 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | pypi-publish: 3 | environment: release 4 | name: Upload release to PyPI 5 | permissions: 6 | id-token: write 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - name: Set up Python 11 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 12 | with: 13 | cache: pip 14 | python-version: 3.x 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install build 19 | - name: Build package 20 | run: python -m build 21 | - name: Publish package distributions to PyPI 22 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 23 | name: Upload Python Package 24 | on: 25 | release: 26 | types: [ published ] 27 | permissions: 28 | contents: read 29 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '36 1 * * 3' 14 | push: 15 | branches: [ "main" ] 16 | # Declare default permissions as read only. 17 | permissions: read-all 18 | jobs: 19 | analysis: 20 | name: Scorecard analysis 21 | runs-on: ubuntu-latest 22 | permissions: 23 | # Needed to upload the results to code-scanning dashboard. 24 | security-events: write 25 | # Needed to publish results and get a badge (see publish_results below). 26 | id-token: write 27 | # Uncomment the permissions below if installing in a private repository. 28 | # contents: read 29 | # actions: read 30 | steps: 31 | - name: "Checkout code" 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | persist-credentials: false 35 | - name: "Run analysis" 36 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 37 | with: 38 | results_file: results.sarif 39 | results_format: sarif 40 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 41 | # - you want to enable the Branch-Protection check on a *public* repository, or 42 | # - you are installing Scorecard on a *private* repository 43 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 44 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 45 | # Public repositories: 46 | # - Publish results to OpenSSF REST API for easy access by consumers 47 | # - Allows the repository to include the Scorecard badge. 48 | # - See https://github.com/ossf/scorecard-action#publishing-results. 49 | # For private repositories: 50 | # - `publish_results` will always be set to `false`, regardless 51 | # of the value entered here. 52 | publish_results: true 53 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 54 | # format to the repository Actions tab. 55 | - name: "Upload artifact" 56 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 57 | with: 58 | name: SARIF file 59 | path: results.sarif 60 | retention-days: 5 61 | # Upload the results to GitHub's code scanning dashboard (optional). 62 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 63 | - name: "Upload to code-scanning" 64 | uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 65 | with: 66 | sarif_file: results.sarif 67 | -------------------------------------------------------------------------------- /.github/workflows/tag_release.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | tag_release: 3 | name: Tag Release 4 | secrets: inherit 5 | uses: praw-dev/.github/.github/workflows/tag_release.yml@main 6 | name: Tag Release 7 | on: 8 | push: 9 | branches: [ main, release_test ] 10 | permissions: 11 | contents: write 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info/ 3 | *.eggs/ 4 | *.pyc 5 | *~ 6 | .DS_Store 7 | .coverage 8 | .idea/ 9 | .python-version 10 | _build/ 11 | build/ 12 | dist/ 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-executables-have-shebangs 7 | - id: check-shebang-scripts-are-executable 8 | - id: check-toml 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: mixed-line-ending 12 | args: [ --fix=no ] 13 | - id: name-tests-test 14 | args: [ --pytest-test-first ] 15 | files: ^tests/integration/.*\.py|tests/unit/.*\.py$ 16 | - id: sort-simple-yaml 17 | files: ^(\.github/workflows/.*\.ya?ml|\.readthedocs.ya?ml)$ 18 | - id: trailing-whitespace 19 | 20 | - repo: https://github.com/pappasam/toml-sort 21 | rev: v0.24.2 22 | hooks: 23 | - id: toml-sort-fix 24 | files: ^(.*\.toml)$ 25 | 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: v0.9.6 28 | hooks: 29 | - id: ruff 30 | args: [ --exit-non-zero-on-fix, --fix ] 31 | files: ^(prawcore/.*.py)$ 32 | - id: ruff-format 33 | 34 | - repo: https://github.com/LilSpazJoekp/docstrfmt 35 | hooks: 36 | - id: docstrfmt 37 | require_serial: true 38 | rev: v1.9.0 39 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Maintainers 2 | =========== 3 | 4 | - Bryce Boe `@bboe `_ 5 | 6 | Contributors 7 | ============ 8 | 9 | - nmtake `@nmtake `_ 10 | - elnuno `@elnuno `_ 11 | - Zeerak Waseem `@ZeerakW `_ 12 | - jarhill0 `@jarhill0 `_ 13 | - Watchful1 `@Watchful1 `_ 14 | - PythonCoderAS `@PythonCoderAS `_ 15 | - LilSpazJoekp `@LilSpazJoekp `_ 16 | - MaybeNetwork `@MaybeNetwork `_ 17 | - Add "Name and github profile link" above this line. 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Bryce Boe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.txt 2 | graft tests 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. _main_page: 2 | 3 | prawcore 4 | ======== 5 | 6 | .. image:: https://img.shields.io/pypi/v/prawcore.svg 7 | :alt: Latest prawcore Version 8 | :target: https://pypi.python.org/pypi/prawcore 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/prawcore 11 | :alt: Supported Python Versions 12 | :target: https://pypi.python.org/pypi/prawcore 13 | 14 | .. image:: https://img.shields.io/pypi/dm/prawcore 15 | :alt: PyPI - Downloads - Monthly 16 | :target: https://pypi.python.org/pypi/prawcore 17 | 18 | .. image:: https://github.com/praw-dev/prawcore/actions/workflows/ci.yml/badge.svg?event=push 19 | :alt: GitHub Actions Status 20 | :target: https://github.com/praw-dev/prawcore/actions/workflows/ci.yml 21 | 22 | .. image:: https://api.securityscorecards.dev/projects/github.com/praw-dev/prawcore/badge 23 | :alt: OpenSSF Scorecard 24 | :target: https://api.securityscorecards.dev/projects/github.com/praw-dev/prawcore 25 | 26 | .. image:: https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg 27 | :alt: Contributor Covenant 28 | :target: https://github.com/praw-dev/.github/blob/main/CODE_OF_CONDUCT.md 29 | 30 | .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white 31 | :alt: pre-commit 32 | :target: https://github.com/pre-commit/pre-commit 33 | 34 | prawcore is a low-level communication layer used by PRAW 4+. 35 | 36 | Installation 37 | ------------ 38 | 39 | Install prawcore using ``pip`` via: 40 | 41 | .. code-block:: console 42 | 43 | pip install prawcore 44 | 45 | Execution Example 46 | ----------------- 47 | 48 | The following example demonstrates how to use prawcore to obtain the list of trophies 49 | for a given user using the script-app type. This example assumes you have the 50 | environment variables ``PRAWCORE_CLIENT_ID`` and ``PRAWCORE_CLIENT_SECRET`` set to the 51 | appropriate values for your application. 52 | 53 | .. code-block:: python 54 | 55 | #!/usr/bin/env python 56 | import os 57 | import pprint 58 | import prawcore 59 | 60 | authenticator = prawcore.TrustedAuthenticator( 61 | prawcore.Requestor("YOUR_VALID_USER_AGENT"), 62 | os.environ["PRAWCORE_CLIENT_ID"], 63 | os.environ["PRAWCORE_CLIENT_SECRET"], 64 | ) 65 | authorizer = prawcore.ReadOnlyAuthorizer(authenticator) 66 | authorizer.refresh() 67 | 68 | with prawcore.session(authorizer) as session: 69 | pprint.pprint(session.request("GET", "/api/v1/user/bboe/trophies")) 70 | 71 | Save the above as ``trophies.py`` and then execute via: 72 | 73 | .. code-block:: console 74 | 75 | python trophies.py 76 | 77 | Additional examples can be found at: 78 | https://github.com/praw-dev/prawcore/tree/main/examples 79 | 80 | Depending on prawcore 81 | --------------------- 82 | 83 | prawcore follows `semantic versioning `_ with the exception that 84 | deprecations will not be preceded by a minor release. In essence, expect only major 85 | versions to introduce breaking changes to prawcore's public interface. As a result, if 86 | you depend on prawcore then it is a good idea to specify not only the minimum version of 87 | prawcore your package requires, but to also limit the major version. 88 | 89 | Below are two examples of how you may want to specify your prawcore dependency: 90 | 91 | setup.py 92 | ~~~~~~~~ 93 | 94 | .. code-block:: python 95 | 96 | setup(..., install_requires=["prawcore >=0.1, <1"], ...) 97 | 98 | requirements.txt 99 | ~~~~~~~~~~~~~~~~ 100 | 101 | .. code-block:: text 102 | 103 | prawcore >=1.5.1, <2 104 | -------------------------------------------------------------------------------- /examples/caching_requestor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example program that shows how simple in-memory caching can be used. 4 | 5 | Demonstrates the use of custom sessions with :class:`.Requestor`. It's an adaptation of 6 | ``read_only_auth_trophies.py``. 7 | 8 | """ 9 | 10 | import os 11 | import sys 12 | 13 | import requests 14 | 15 | import prawcore 16 | 17 | 18 | class CachingSession(requests.Session): 19 | """Cache GETs in memory. 20 | 21 | Toy example of custom session to showcase the ``session`` parameter of 22 | :class:`.Requestor`. 23 | 24 | """ 25 | 26 | get_cache = {} 27 | 28 | def request(self, method, url, params=None, **kwargs): 29 | """Perform a request, or return a cached response if available.""" 30 | params_key = tuple(params.items()) if params else () 31 | if method.upper() == "GET" and (url, params_key) in self.get_cache: 32 | print("Returning cached response for:", method, url, params) 33 | return self.get_cache[(url, params_key)] 34 | result = super().request(method, url, params, **kwargs) 35 | if method.upper() == "GET": 36 | self.get_cache[(url, params_key)] = result 37 | print("Adding entry to the cache:", method, url, params) 38 | return result 39 | 40 | 41 | def main(): 42 | """Provide the program's entry point when directly executed.""" 43 | if len(sys.argv) != 2: 44 | print(f"Usage: {sys.argv[0]} USERNAME") 45 | return 1 46 | 47 | caching_requestor = prawcore.Requestor("prawcore_device_id_auth_example", session=CachingSession()) 48 | authenticator = prawcore.TrustedAuthenticator( 49 | caching_requestor, 50 | os.environ["PRAWCORE_CLIENT_ID"], 51 | os.environ["PRAWCORE_CLIENT_SECRET"], 52 | ) 53 | authorizer = prawcore.ReadOnlyAuthorizer(authenticator) 54 | authorizer.refresh() 55 | 56 | user = sys.argv[1] 57 | with prawcore.session(authorizer) as session: 58 | data1 = session.request("GET", f"/api/v1/user/{user}/trophies") 59 | 60 | with prawcore.session(authorizer) as session: 61 | data2 = session.request("GET", f"/api/v1/user/{user}/trophies") 62 | 63 | for trophy in data1["data"]["trophies"]: 64 | description = trophy["data"]["description"] 65 | print( 66 | "Original:", 67 | trophy["data"]["name"] + (f" ({description})" if description else ""), 68 | ) 69 | 70 | for trophy in data2["data"]["trophies"]: 71 | description = trophy["data"]["description"] 72 | print( 73 | "Cached:", 74 | trophy["data"]["name"] + (f" ({description})" if description else ""), 75 | ) 76 | print( 77 | "----\nCached == Original:", 78 | data2["data"]["trophies"] == data2["data"]["trophies"], 79 | ) 80 | 81 | return 0 82 | 83 | 84 | if __name__ == "__main__": 85 | sys.exit(main()) 86 | -------------------------------------------------------------------------------- /examples/device_id_auth_trophies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example program that outputs a user's list of trophies. 4 | 5 | This program demonstrates the use of ``prawcore.DeviceIDAuthorizer``. 6 | 7 | """ 8 | 9 | import os 10 | import sys 11 | 12 | import prawcore 13 | 14 | 15 | def main(): 16 | """Provide the program's entry point when directly executed.""" 17 | if len(sys.argv) != 2: 18 | print(f"Usage: {sys.argv[0]} USERNAME") 19 | return 1 20 | 21 | authenticator = prawcore.UntrustedAuthenticator( 22 | prawcore.Requestor("prawcore_device_id_auth_example"), 23 | os.environ["PRAWCORE_CLIENT_ID"], 24 | ) 25 | authorizer = prawcore.DeviceIDAuthorizer(authenticator) 26 | authorizer.refresh() 27 | 28 | user = sys.argv[1] 29 | with prawcore.session(authorizer) as session: 30 | data = session.request("GET", f"/api/v1/user/{user}/trophies") 31 | 32 | for trophy in data["data"]["trophies"]: 33 | description = trophy["data"]["description"] 34 | print(trophy["data"]["name"] + (f" ({description})" if description else "")) 35 | 36 | return 0 37 | 38 | 39 | if __name__ == "__main__": 40 | sys.exit(main()) 41 | -------------------------------------------------------------------------------- /examples/obtain_refresh_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example program that demonstrates the flow for retrieving a refresh token. 4 | 5 | In order for this example to work your application's redirect URI must be set to 6 | http://localhost:8080. 7 | 8 | This tool can be used to conveniently create refresh tokens for later use with your web 9 | application OAuth2 credentials. 10 | 11 | """ 12 | 13 | import os 14 | import random 15 | import socket 16 | import sys 17 | 18 | import prawcore 19 | 20 | 21 | def receive_connection(): 22 | """Wait for and then return a connected socket.. 23 | 24 | Opens a TCP connection on port 8080, and waits for a single client. 25 | 26 | """ 27 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 28 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 29 | server.bind(("localhost", 8080)) 30 | server.listen(1) 31 | client = server.accept()[0] 32 | server.close() 33 | return client 34 | 35 | 36 | def send_message(client, message): 37 | """Send message to client and close the connection.""" 38 | print(message) 39 | client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode()) 40 | client.close() 41 | 42 | 43 | def main(): 44 | """Provide the program's entry point when directly executed.""" 45 | if len(sys.argv) < 2: 46 | print(f"Usage: {sys.argv[0]} SCOPE...") 47 | return 1 48 | 49 | authenticator = prawcore.TrustedAuthenticator( 50 | prawcore.Requestor("prawcore_refresh_token_example"), 51 | os.environ["PRAWCORE_CLIENT_ID"], 52 | os.environ["PRAWCORE_CLIENT_SECRET"], 53 | "http://localhost:8080", 54 | ) 55 | 56 | state = str(random.randint(0, 65000)) # noqa: S311 57 | url = authenticator.authorize_url("permanent", sys.argv[1:], state) 58 | print(url) 59 | 60 | client = receive_connection() 61 | data = client.recv(1024).decode("utf-8") 62 | param_tokens = data.split(" ", 2)[1].split("?", 1)[1].split("&") 63 | params = dict([token.split("=") for token in param_tokens]) 64 | 65 | if state != params["state"]: 66 | send_message( 67 | client, 68 | f"State mismatch. Expected: {state} Received: {params['state']}", 69 | ) 70 | return 1 71 | if "error" in params: 72 | send_message(client, params["error"]) 73 | return 1 74 | 75 | authorizer = prawcore.Authorizer(authenticator) 76 | authorizer.authorize(params["code"]) 77 | 78 | send_message(client, f"Refresh token: {authorizer.refresh_token}") 79 | return 0 80 | 81 | 82 | if __name__ == "__main__": 83 | sys.exit(main()) 84 | -------------------------------------------------------------------------------- /examples/read_only_auth_trophies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example program that outputs a user's list of trophies. 4 | 5 | This program demonstrates the use of ``prawcore.ReadOnlyAuthorizer`` that does not 6 | require an access token to make authenticated requests to Reddit. 7 | 8 | """ 9 | 10 | import os 11 | import sys 12 | 13 | import prawcore 14 | 15 | 16 | def main(): 17 | """Provide the program's entry point when directly executed.""" 18 | if len(sys.argv) != 2: 19 | print(f"Usage: {sys.argv[0]} USERNAME") 20 | return 1 21 | 22 | authenticator = prawcore.TrustedAuthenticator( 23 | prawcore.Requestor("prawcore_read_only_example"), 24 | os.environ["PRAWCORE_CLIENT_ID"], 25 | os.environ["PRAWCORE_CLIENT_SECRET"], 26 | ) 27 | authorizer = prawcore.ReadOnlyAuthorizer(authenticator) 28 | authorizer.refresh() 29 | 30 | user = sys.argv[1] 31 | with prawcore.session(authorizer) as session: 32 | data = session.request("GET", f"/api/v1/user/{user}/trophies") 33 | 34 | for trophy in data["data"]["trophies"]: 35 | description = trophy["data"]["description"] 36 | print(trophy["data"]["name"] + (f" ({description})" if description else "")) 37 | 38 | return 0 39 | 40 | 41 | if __name__ == "__main__": 42 | sys.exit(main()) 43 | -------------------------------------------------------------------------------- /examples/script_auth_friend_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """script_auth_friend_list.py outputs the authenticated user's list of friends. 4 | 5 | This program demonstrates the use of ``prawcore.ScriptAuthorizer``, which enables those 6 | listed as a developer of the application to authenticate using their username and 7 | password. 8 | 9 | """ 10 | 11 | import os 12 | import sys 13 | 14 | import prawcore 15 | 16 | 17 | def main(): 18 | """Provide the program's entry point when directly executed.""" 19 | authenticator = prawcore.TrustedAuthenticator( 20 | prawcore.Requestor("prawcore_script_auth_example"), 21 | os.environ["PRAWCORE_CLIENT_ID"], 22 | os.environ["PRAWCORE_CLIENT_SECRET"], 23 | ) 24 | authorizer = prawcore.ScriptAuthorizer( 25 | authenticator, 26 | os.environ["PRAWCORE_USERNAME"], 27 | os.environ["PRAWCORE_PASSWORD"], 28 | ) 29 | authorizer.refresh() 30 | 31 | with prawcore.session(authorizer) as session: 32 | data = session.request("GET", "/api/v1/me/friends") 33 | 34 | for friend in data["data"]["children"]: 35 | print(friend["name"]) 36 | 37 | return 0 38 | 39 | 40 | if __name__ == "__main__": 41 | sys.exit(main()) 42 | -------------------------------------------------------------------------------- /prawcore/__init__.py: -------------------------------------------------------------------------------- 1 | """Low-level communication layer for PRAW 4+.""" 2 | 3 | import logging 4 | 5 | from .auth import ( 6 | Authorizer, 7 | DeviceIDAuthorizer, 8 | ImplicitAuthorizer, 9 | ReadOnlyAuthorizer, 10 | ScriptAuthorizer, 11 | TrustedAuthenticator, 12 | UntrustedAuthenticator, 13 | ) 14 | from .exceptions import * # noqa: F403 15 | from .requestor import Requestor 16 | from .sessions import Session, session 17 | 18 | logging.getLogger(__package__).addHandler(logging.NullHandler()) 19 | 20 | __version__ = "3.0.3.dev0" 21 | -------------------------------------------------------------------------------- /prawcore/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the prawcore package.""" 2 | 3 | import os 4 | 5 | ACCESS_TOKEN_PATH = "/api/v1/access_token" # noqa: S105 6 | AUTHORIZATION_PATH = "/api/v1/authorize" # noqa: S105 7 | NANOSECONDS = 1_000_000_000 # noqa: S105 8 | REVOKE_TOKEN_PATH = "/api/v1/revoke_token" # noqa: S105 9 | TIMEOUT = float( 10 | os.environ.get( 11 | "PRAWCORE_TIMEOUT", 12 | os.environ.get("prawcore_timeout", 16), # noqa: SIM112 13 | ) 14 | ) 15 | WINDOW_SIZE = 600 16 | -------------------------------------------------------------------------------- /prawcore/exceptions.py: -------------------------------------------------------------------------------- 1 | """Provide exception classes for the prawcore package.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Any 6 | from urllib.parse import urlparse 7 | 8 | if TYPE_CHECKING: 9 | from requests.models import Response 10 | 11 | 12 | class PrawcoreException(Exception): # noqa: N818 13 | """Base exception class for exceptions that occur within this package.""" 14 | 15 | 16 | class InvalidInvocation(PrawcoreException): 17 | """Indicate that the code to execute cannot be completed.""" 18 | 19 | 20 | class OAuthException(PrawcoreException): 21 | """Indicate that there was an OAuth2 related error with the request.""" 22 | 23 | def __init__(self, response: Response, error: str, description: str | None = None) -> None: 24 | """Initialize a OAuthException instance. 25 | 26 | :param response: A ``requests.response`` instance. 27 | :param error: The error type returned by Reddit. 28 | :param description: A description of the error when provided. 29 | 30 | """ 31 | self.error = error 32 | self.description = description 33 | self.response = response 34 | message = f"{error} error processing request" 35 | if description: 36 | message += f" ({description})" 37 | PrawcoreException.__init__(self, message) 38 | 39 | 40 | class RequestException(PrawcoreException): 41 | """Indicate that there was an error with the incomplete HTTP request.""" 42 | 43 | def __init__( 44 | self, 45 | original_exception: Exception, 46 | request_args: tuple[Any, ...], 47 | request_kwargs: dict[str, bool | (dict[str, int] | (dict[str, str] | str)) | None], 48 | ) -> None: 49 | """Initialize a RequestException instance. 50 | 51 | :param original_exception: The original exception that occurred. 52 | :param request_args: The arguments to the request function. 53 | :param request_kwargs: The keyword arguments to the request function. 54 | 55 | """ 56 | self.original_exception = original_exception 57 | self.request_args = request_args 58 | self.request_kwargs = request_kwargs 59 | super().__init__(f"error with request {original_exception}") 60 | 61 | 62 | class ResponseException(PrawcoreException): 63 | """Indicate that there was an error with the completed HTTP request.""" 64 | 65 | def __init__(self, response: Response) -> None: 66 | """Initialize a ResponseException instance. 67 | 68 | :param response: A ``requests.response`` instance. 69 | 70 | """ 71 | self.response = response 72 | super().__init__(f"received {response.status_code} HTTP response") 73 | 74 | 75 | class BadJSON(ResponseException): 76 | """Indicate the response did not contain valid JSON.""" 77 | 78 | 79 | class BadRequest(ResponseException): 80 | """Indicate invalid parameters for the request.""" 81 | 82 | 83 | class Conflict(ResponseException): 84 | """Indicate a conflicting change in the target resource.""" 85 | 86 | 87 | class Forbidden(ResponseException): 88 | """Indicate the authentication is not permitted for the request.""" 89 | 90 | 91 | class InsufficientScope(ResponseException): 92 | """Indicate that the request requires a different scope.""" 93 | 94 | 95 | class InvalidToken(ResponseException): 96 | """Indicate that the request used an invalid access token.""" 97 | 98 | 99 | class NotFound(ResponseException): 100 | """Indicate that the requested URL was not found.""" 101 | 102 | 103 | class Redirect(ResponseException): 104 | """Indicate the request resulted in a redirect. 105 | 106 | This class adds the attribute ``path``, which is the path to which the response 107 | redirects. 108 | 109 | """ 110 | 111 | def __init__(self, response: Response) -> None: 112 | """Initialize a Redirect exception instance. 113 | 114 | :param response: A ``requests.response`` instance containing a location header. 115 | 116 | """ 117 | path = urlparse(response.headers["location"]).path 118 | self.path = path[:-5] if path.endswith(".json") else path 119 | self.response = response 120 | msg = f"Redirect to {self.path}" 121 | msg += ( 122 | " (You may be trying to perform a non-read-only action via a read-only instance.)" 123 | if "/login/" in self.path 124 | else "" 125 | ) 126 | PrawcoreException.__init__(self, msg) 127 | 128 | 129 | class ServerError(ResponseException): 130 | """Indicate issues on the server end preventing request fulfillment.""" 131 | 132 | 133 | class SpecialError(ResponseException): 134 | """Indicate syntax or spam-prevention issues.""" 135 | 136 | def __init__(self, response: Response) -> None: 137 | """Initialize a SpecialError exception instance. 138 | 139 | :param response: A ``requests.response`` instance containing a message and a 140 | list of special errors. 141 | 142 | """ 143 | self.response = response 144 | 145 | resp_dict = self.response.json() # assumes valid JSON 146 | self.message = resp_dict.get("message", "") 147 | self.reason = resp_dict.get("reason", "") 148 | self.special_errors = resp_dict.get("special_errors", []) 149 | PrawcoreException.__init__(self, f"Special error {self.message!r}") 150 | 151 | 152 | class TooLarge(ResponseException): 153 | """Indicate that the request data exceeds the allowed limit.""" 154 | 155 | 156 | class TooManyRequests(ResponseException): 157 | """Indicate that the user has sent too many requests in a given amount of time.""" 158 | 159 | def __init__(self, response: Response) -> None: 160 | """Initialize a TooManyRequests exception instance. 161 | 162 | :param response: A ``requests.response`` instance that may contain a retry-after 163 | header and a message. 164 | 165 | """ 166 | self.response = response 167 | self.retry_after = response.headers.get("retry-after") 168 | self.message = response.text # Not all response bodies are valid JSON 169 | 170 | msg = f"received {response.status_code} HTTP response" 171 | if self.retry_after: 172 | msg += f". Please wait at least {float(self.retry_after)} seconds before re-trying this request." 173 | PrawcoreException.__init__(self, msg) 174 | 175 | 176 | class URITooLong(ResponseException): 177 | """Indicate that the length of the request URI exceeds the allowed limit.""" 178 | 179 | 180 | class UnavailableForLegalReasons(ResponseException): 181 | """Indicate that the requested URL is unavailable due to legal reasons.""" 182 | -------------------------------------------------------------------------------- /prawcore/rate_limit.py: -------------------------------------------------------------------------------- 1 | """Provide the RateLimiter class.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import time 7 | from typing import TYPE_CHECKING, Any, Callable 8 | 9 | if TYPE_CHECKING: 10 | from collections.abc import Mapping 11 | 12 | from requests.models import Response 13 | 14 | from prawcore.const import NANOSECONDS 15 | 16 | log = logging.getLogger(__package__) 17 | 18 | 19 | class RateLimiter: 20 | """Facilitates the rate limiting of requests to Reddit. 21 | 22 | Rate limits are controlled based on feedback from requests to Reddit. 23 | 24 | """ 25 | 26 | def __init__(self, *, window_size: int) -> None: 27 | """Create an instance of the RateLimit class.""" 28 | self.remaining: int | None = None 29 | self.next_request_timestamp_ns: int | None = None 30 | self.used: int | None = None 31 | self.window_size: int = window_size 32 | 33 | def call( 34 | self, 35 | request_function: Callable[[Any], Response], 36 | set_header_callback: Callable[[], dict[str, str]], 37 | *args: Any, 38 | **kwargs: Any, 39 | ) -> Response: 40 | """Rate limit the call to ``request_function``. 41 | 42 | :param request_function: A function call that returns an HTTP response object. 43 | :param set_header_callback: A callback function used to set the request headers. 44 | This callback is called after any necessary sleep time occurs. 45 | :param args: The positional arguments to ``request_function``. 46 | :param kwargs: The keyword arguments to ``request_function``. 47 | 48 | """ 49 | self.delay() 50 | kwargs["headers"] = set_header_callback() 51 | response = request_function(*args, **kwargs) 52 | self.update(response.headers) 53 | return response 54 | 55 | def delay(self) -> None: 56 | """Sleep for an amount of time to remain under the rate limit.""" 57 | if self.next_request_timestamp_ns is None: 58 | return 59 | sleep_seconds = float(self.next_request_timestamp_ns - time.monotonic_ns()) / NANOSECONDS 60 | if sleep_seconds <= 0: 61 | return 62 | message = f"Sleeping: {sleep_seconds:0.2f} seconds prior to call" 63 | log.debug(message) 64 | time.sleep(sleep_seconds) 65 | 66 | def update(self, response_headers: Mapping[str, str]) -> None: 67 | """Update the state of the rate limiter based on the response headers. 68 | 69 | This method should only be called following an HTTP request to Reddit. 70 | 71 | Response headers that do not contain ``x-ratelimit`` fields will be treated as a 72 | single request. This behavior is to error on the safe-side as such responses 73 | should trigger exceptions that indicate invalid behavior. 74 | 75 | """ 76 | if "x-ratelimit-remaining" not in response_headers: 77 | if self.remaining is not None and self.used is not None: 78 | self.remaining -= 1 79 | self.used += 1 80 | return 81 | 82 | self.remaining = int(float(response_headers["x-ratelimit-remaining"])) 83 | self.used = int(response_headers["x-ratelimit-used"]) 84 | 85 | now_ns = time.monotonic_ns() 86 | seconds_to_reset = int(response_headers["x-ratelimit-reset"]) 87 | 88 | if self.remaining <= 0: 89 | self.next_request_timestamp_ns = now_ns + max(NANOSECONDS, seconds_to_reset * NANOSECONDS) 90 | return 91 | 92 | self.next_request_timestamp_ns = int( 93 | now_ns 94 | + min( 95 | seconds_to_reset, 96 | max( 97 | seconds_to_reset 98 | - (self.window_size - self.window_size / (float(self.remaining) + self.used) * self.used), 99 | 0, 100 | ), 101 | 10, 102 | ) 103 | * NANOSECONDS 104 | ) 105 | -------------------------------------------------------------------------------- /prawcore/requestor.py: -------------------------------------------------------------------------------- 1 | """Provides the HTTP request handling interface.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Any 6 | 7 | import requests 8 | 9 | from .const import TIMEOUT 10 | from .exceptions import InvalidInvocation, RequestException 11 | 12 | if TYPE_CHECKING: 13 | from requests import Response, Session 14 | 15 | 16 | class Requestor: 17 | """Requestor provides an interface to HTTP requests.""" 18 | 19 | MIN_USER_AGENT_LENGTH = 7 20 | 21 | def __getattr__(self, attribute: str) -> object: 22 | """Pass all undefined attributes to the ``_http`` attribute.""" 23 | if attribute.startswith("__"): 24 | raise AttributeError 25 | return getattr(self._http, attribute) 26 | 27 | def __init__( 28 | self, 29 | user_agent: str, 30 | oauth_url: str = "https://oauth.reddit.com", 31 | reddit_url: str = "https://www.reddit.com", 32 | session: Session | None = None, 33 | timeout: float = TIMEOUT, 34 | ) -> None: 35 | """Create an instance of the Requestor class. 36 | 37 | :param user_agent: The user-agent for your application. Please follow Reddit's 38 | user-agent guidelines: https://github.com/reddit/reddit/wiki/API#rules 39 | :param oauth_url: The URL used to make OAuth requests to the Reddit site 40 | (default: ``"https://oauth.reddit.com"``). 41 | :param reddit_url: The URL used when obtaining access tokens (default: 42 | ``"https://www.reddit.com"``). 43 | :param session: A session instance to handle requests, compatible with 44 | ``requests.Session()`` (default: ``None``). 45 | :param timeout: How many seconds to wait for the server to send data before 46 | giving up (default: ``prawcore.const.TIMEOUT``). 47 | 48 | """ 49 | # Imported locally to avoid an import cycle, with __init__ 50 | from . import __version__ 51 | 52 | if user_agent is None or len(user_agent) < self.MIN_USER_AGENT_LENGTH: 53 | msg = "user_agent is not descriptive" 54 | raise InvalidInvocation(msg) 55 | 56 | self._http = session or requests.Session() 57 | self._http.headers["User-Agent"] = f"{user_agent} prawcore/{__version__}" 58 | 59 | self.oauth_url = oauth_url 60 | self.reddit_url = reddit_url 61 | self.timeout = timeout 62 | 63 | def close(self) -> None: 64 | """Call close on the underlying session.""" 65 | self._http.close() 66 | 67 | def request(self, *args: Any, timeout: float | None = None, **kwargs: Any) -> Response: 68 | """Issue the HTTP request capturing any errors that may occur.""" 69 | try: 70 | return self._http.request(*args, timeout=timeout or self.timeout, **kwargs) 71 | except Exception as exc: # noqa: BLE001 72 | raise RequestException(exc, args, kwargs) from None 73 | -------------------------------------------------------------------------------- /prawcore/util.py: -------------------------------------------------------------------------------- 1 | """Provide utility for the prawcore package.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from .exceptions import Forbidden, InsufficientScope, InvalidToken 8 | 9 | if TYPE_CHECKING: 10 | from requests.models import Response 11 | 12 | _auth_error_mapping = { 13 | 403: Forbidden, 14 | "insufficient_scope": InsufficientScope, 15 | "invalid_token": InvalidToken, 16 | } 17 | 18 | 19 | def authorization_error_class( 20 | response: Response, 21 | ) -> InvalidToken | (Forbidden | InsufficientScope): 22 | """Return an exception instance that maps to the OAuth Error. 23 | 24 | :param response: The HTTP response containing a www-authenticate error. 25 | 26 | """ 27 | message = response.headers.get("www-authenticate") 28 | error: int | str 29 | error = message.replace('"', "").rsplit("=", 1)[1] if message else response.status_code 30 | return _auth_error_mapping[error](response) 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "flit_core.buildapi" 3 | requires = ["flit_core >=3.4,<4"] 4 | 5 | [dependency-groups] 6 | dev = [ 7 | "tox-uv>=1.22.1" 8 | ] 9 | lint = [ 10 | "pre-commit", 11 | "ruff>=0.9.5" 12 | ] 13 | test = [ 14 | "betamax >=0.8, <0.9", 15 | "coverage>=7.6.10", 16 | "pytest>=8.3.4", 17 | "urllib3 ==1.*" 18 | ] 19 | type = [ 20 | "pyright>=1.1.393", 21 | "pytest>=8.3.4" 22 | ] 23 | 24 | [project] 25 | authors = [{name = "Bryce Boe", email = "bbzbryce@gmail.com"}] 26 | classifiers = [ 27 | "Development Status :: 5 - Production/Stable", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: BSD License", 30 | "Operating System :: OS Independent", 31 | "Natural Language :: English", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Programming Language :: Python :: 3.13" 39 | ] 40 | dependencies = [ 41 | "requests >=2.6.0, <3.0" 42 | ] 43 | dynamic = ["version", "description"] 44 | keywords = ["praw", "reddit", "api"] 45 | license = {file = "LICENSE.txt"} 46 | maintainers = [ 47 | {name = "Bryce Boe", email = "bbzbryce@gmail.com"} 48 | ] 49 | name = "prawcore" 50 | readme = "README.rst" 51 | requires-python = "~=3.9" 52 | 53 | [project.urls] 54 | "Issue Tracker" = "https://github.com/praw-dev/prawcore/issues" 55 | "Source Code" = "https://github.com/praw-dev/prawcore" 56 | 57 | [tool.ruff] 58 | include = [ 59 | "examples/*.py", 60 | "prawcore/*.py", 61 | "tests/*.py" 62 | ] 63 | line-length = 120 64 | 65 | [tool.ruff.lint] 66 | ignore = [ 67 | "A002", # shadowing built-in name 68 | "A004", # shadowing built-in 69 | "ANN202", # missing return type for private method 70 | "D203", # 1 blank line required before class docstring 71 | "D213", # Multi-line docstring summary should start at the second line 72 | "E501", # line-length 73 | "PLR0913" # too many arguments 74 | ] 75 | select = [ 76 | "A", # flake8-builtins 77 | "ANN", # flake8-annotations 78 | "ARG", # flake8-unused-arguments 79 | "B", # flake8-bugbear 80 | "BLE", # flake8-blind-except 81 | "C4", # flake8-comprehensions 82 | "D", # pydocstyle 83 | "DTZ", # flake8-datetimez 84 | "E", # pycodestyle errors 85 | "EM", # flake8-errmsg 86 | "ERA", # eradicate 87 | "EXE", # flake8-executable 88 | "F", # pyflakes 89 | "FA", # flake8-future-annotations 90 | "FIX", # flake8-fix me 91 | "FLY", # flynt 92 | "G", # flake8-logging-format 93 | "I", # isort 94 | "INP", # flake8-no-pep420 95 | "ISC", # flake8-implicit-str-concat 96 | "N", # pep8-naming 97 | "PIE", # flake8-pie 98 | "PGH", # pygrep-hooks 99 | "PL", # Pylint 100 | "PT", # flake8-pytest-style 101 | "PTH", # flake8-use-pathlib 102 | "PYI", # flake8-pyi 103 | "Q", # flake8-quotes 104 | "RET", # flake8-return 105 | "RSE", # flake8-raise 106 | "S", # bandit 107 | "SIM", # flake8-simplify 108 | "T10", # flake8-debugger 109 | "T20", # flake8-print 110 | "TCH", # flake8-type-checking 111 | "TD", # flake8-todos 112 | "W", # pycodestyle warnings 113 | "UP" # pyupgrade 114 | ] 115 | 116 | [tool.ruff.lint.flake8-annotations] 117 | allow-star-arg-any = true 118 | suppress-dummy-args = true 119 | 120 | [tool.ruff.lint.per-file-ignores] 121 | "__init__.py" = ["F401"] 122 | "examples/*.py" = ["ANN", "PLR2004", "T201"] 123 | "tests/**.py" = ["ANN", "D", "PLR2004", "S101", "S105", "S106", "S301"] 124 | 125 | [tool.tox] 126 | envlist = ["py39", "py310", "py311", "py312", "py313", "pre-commit", "style", "type"] 127 | minversion = "4.22" 128 | 129 | [tool.tox.env.pre-commit] 130 | commands = [ 131 | ["pre-commit", "run", "--all-files"] 132 | ] 133 | description = "run pre-commit on code base" 134 | dependency_groups = ["lint"] 135 | runner = "uv-venv-lock-runner" 136 | 137 | [tool.tox.env.style] 138 | commands = [ 139 | ["ruff", "check"], 140 | ["ruff", "format", "--diff", "--target-version", "py39"] 141 | ] 142 | description = "run lint check on code base" 143 | dependency_groups = ["lint"] 144 | runner = "uv-venv-lock-runner" 145 | 146 | [tool.tox.env.stylefix] 147 | commands = [ 148 | ["ruff", "check", "--fix"], 149 | ["ruff", "format", "--target-version", "py39"] 150 | ] 151 | description = "run lint check on code base" 152 | dependency_groups = ["lint"] 153 | runner = "uv-venv-lock-runner" 154 | 155 | [tool.tox.env.type] 156 | commands = [["pyright", "prawcore"]] 157 | dependency_groups = ["type"] 158 | description = "run type check on code base" 159 | runner = "uv-venv-lock-runner" 160 | 161 | [tool.tox.env_run_base] 162 | commands = [ 163 | ["coverage", "run", "-m", "pytest", "{posargs}"], 164 | ["coverage", "report", "-m", "--fail-under=100"] 165 | ] 166 | description = "Run test under {base_python}" 167 | dependency_groups = ["test"] 168 | runner = "uv-venv-lock-runner" 169 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """prawcore Test Suite.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Prepare pytest.""" 2 | 3 | import os 4 | import socket 5 | import time 6 | from base64 import b64encode 7 | from sys import platform 8 | 9 | import pytest 10 | 11 | from prawcore import Requestor, TrustedAuthenticator, UntrustedAuthenticator 12 | 13 | 14 | @pytest.fixture(autouse=True) 15 | def patch_sleep(monkeypatch): 16 | """Auto patch sleep to speed up tests.""" 17 | 18 | def _sleep(*_, **__): 19 | """Dud sleep function.""" 20 | 21 | monkeypatch.setattr(time, "sleep", value=_sleep) 22 | 23 | 24 | @pytest.fixture 25 | def requestor(): 26 | """Return path to image.""" 27 | return Requestor("prawcore:test (by /u/bboe)") 28 | 29 | 30 | @pytest.fixture 31 | def trusted_authenticator(requestor): 32 | """Return a TrustedAuthenticator instance.""" 33 | return TrustedAuthenticator( 34 | requestor, 35 | pytest.placeholders.client_id, 36 | pytest.placeholders.client_secret, 37 | ) 38 | 39 | 40 | @pytest.fixture 41 | def untrusted_authenticator(requestor): 42 | """Return an UntrustedAuthenticator instance.""" 43 | return UntrustedAuthenticator(requestor, pytest.placeholders.client_id) 44 | 45 | 46 | def env_default(key): 47 | """Return environment variable or placeholder string.""" 48 | return os.environ.get( 49 | f"PRAWCORE_{key.upper()}", 50 | "http://localhost:8080" if key == "redirect_uri" else f"fake_{key}", 51 | ) 52 | 53 | 54 | def pytest_configure(config): 55 | pytest.placeholders = Placeholders(placeholders) 56 | config.addinivalue_line("markers", "cassette_name: Name of cassette to use for test.") 57 | config.addinivalue_line("markers", "recorder_kwargs: Arguments to pass to the recorder.") 58 | 59 | 60 | class Placeholders: 61 | def __init__(self, _dict): 62 | self.__dict__ = _dict 63 | 64 | 65 | placeholders = { 66 | x: env_default(x) 67 | for x in ( 68 | "client_id client_secret password permanent_grant_code temporary_grant_code" 69 | " redirect_uri refresh_token user_agent username" 70 | ).split() 71 | } 72 | 73 | if ( 74 | placeholders["client_id"] != "fake_client_id" and placeholders["client_secret"] == "fake_client_secret" 75 | ): # pragma: no cover 76 | placeholders["basic_auth"] = b64encode(f"{placeholders['client_id']}:".encode()).decode("utf-8") 77 | else: 78 | placeholders["basic_auth"] = b64encode( 79 | f"{placeholders['client_id']}:{placeholders['client_secret']}".encode() 80 | ).decode("utf-8") 81 | 82 | 83 | if platform == "darwin": # Work around issue with betamax on OS X # pragma: no cover 84 | socket.gethostbyname = lambda _: "127.0.0.1" 85 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """prawcore Integration test suite.""" 2 | 3 | import os 4 | from urllib.parse import quote_plus 5 | 6 | import betamax 7 | import pytest 8 | from betamax.cassette import Cassette 9 | 10 | from ..utils import ( 11 | PrettyJSONSerializer, 12 | ensure_integration_test, 13 | filter_access_token, 14 | ) 15 | 16 | CASSETTES_PATH = "tests/integration/cassettes" 17 | existing_cassettes = set() 18 | used_cassettes = set() 19 | 20 | 21 | class IntegrationTest: 22 | """Base class for prawcore integration tests.""" 23 | 24 | @pytest.fixture(autouse=True, scope="session") 25 | def cassette_tracker(self): # pragma: no cover 26 | """Track cassettes to ensure unused cassettes are not uploaded.""" 27 | for cassette in os.listdir(CASSETTES_PATH): 28 | existing_cassettes.add(cassette[: cassette.rindex(".")]) 29 | yield 30 | unused_cassettes = existing_cassettes - used_cassettes 31 | if unused_cassettes and os.getenv("ENSURE_NO_UNUSED_CASSETTES", "0") == "1": 32 | msg = f"The following cassettes are unused: {', '.join(unused_cassettes)}." 33 | raise AssertionError(msg) 34 | 35 | @pytest.fixture(autouse=True) 36 | def cassette(self, request, recorder, cassette_name): 37 | """Wrap a test in a Betamax cassette.""" 38 | kwargs = {} 39 | for marker in request.node.iter_markers("recorder_kwargs"): 40 | for key, value in marker.kwargs.items(): 41 | # Don't overwrite existing values since function markers are provided 42 | # before class markers. 43 | kwargs.setdefault(key, value) 44 | with recorder.use_cassette(cassette_name, **kwargs) as recorder_context: 45 | cassette = recorder_context.current_cassette 46 | yield recorder_context 47 | ensure_integration_test(cassette) 48 | used_cassettes.add(cassette_name) 49 | 50 | @pytest.fixture(autouse=True) 51 | def recorder(self, requestor): 52 | """Configure Betamax.""" 53 | recorder = betamax.Betamax(requestor) 54 | recorder.register_serializer(PrettyJSONSerializer) 55 | with betamax.Betamax.configure() as config: 56 | config.cassette_library_dir = CASSETTES_PATH 57 | config.default_cassette_options["serialize_with"] = "prettyjson" 58 | config.before_record(callback=filter_access_token) 59 | for key, value in pytest.placeholders.__dict__.items(): 60 | if key == "password": 61 | value = quote_plus(value) # noqa: PLW2901 62 | config.define_cassette_placeholder(f"<{key.upper()}>", value) 63 | yield recorder 64 | # since placeholders persist between tests 65 | Cassette.default_cassette_options["placeholders"] = [] 66 | 67 | @pytest.fixture 68 | def cassette_name(self, request): 69 | """Return the name of the cassette to use.""" 70 | marker = request.node.get_closest_marker("cassette_name") 71 | if marker is None: 72 | return f"{request.cls.__name__}.{request.node.name}" if request.cls else request.node.name 73 | return marker.args[0] 74 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_authorize__with_invalid_code.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T20:31:23", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "code=invalid+code&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "90", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "User-Agent": "prawcore/0.0.1a1" 18 | }, 19 | "method": "POST", 20 | "uri": "https://www.reddit.com/api/v1/access_token" 21 | }, 22 | "response": { 23 | "body": { 24 | "encoding": "UTF-8", 25 | "string": "{\"error\": \"invalid_grant\"}" 26 | }, 27 | "headers": { 28 | "CF-RAY": "274b89323c1039e2-PHX", 29 | "Connection": "keep-alive", 30 | "Content-Length": "26", 31 | "Content-Type": "application/json; charset=UTF-8", 32 | "Date": "Sun, 14 Feb 2016 20:31:19 GMT", 33 | "Server": "cloudflare-nginx", 34 | "Set-Cookie": "__cfduid=d85ee693995a0b47572b2d5fa2f438f191455481879; expires=Mon, 13-Feb-17 20:31:19 GMT; path=/; domain=.reddit.com; HttpOnly", 35 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 36 | "X-Moose": "majestic", 37 | "cache-control": "max-age=0, must-revalidate", 38 | "x-content-type-options": "nosniff", 39 | "x-frame-options": "SAMEORIGIN", 40 | "x-xss-protection": "1; mode=block" 41 | }, 42 | "status": { 43 | "code": 200, 44 | "message": "OK" 45 | }, 46 | "url": "https://www.reddit.com/api/v1/access_token" 47 | } 48 | } 49 | ], 50 | "recorded_with": "betamax/0.5.1" 51 | } 52 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_authorize__with_permanent_grant.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T20:58:01", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "code=&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "105", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=d85ee693995a0b47572b2d5fa2f438f191455481879", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA2XNQQuCQBQE4L8i72wgCVrdt0gjJRCsi5g+a3N1t31brET/PbaOXYeZb15QNw0SVUb2OMLKgzgM5osonj2T246ptc3URqbigJdHQfbOBeU9+B58+5WZFLrRGWuN2uVoFddIFXdYGAWB74HGTiNd/z+WrM+PW9apdE9JiWVW8DEaBitOzFnUyB/PWxwNNxO8P3GeaqiwAAAA", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "274bb035f502399a-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 14 Feb 2016 20:57:57 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.5.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_authorize__with_temporary_grant.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T21:00:36", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "code=&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "105", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=d85ee693995a0b47572b2d5fa2f438f191455481879", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXtQwzjTQMSk4pzPZK9SoO9EzKCksrt4j3CDV2VdJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjLTEnNK8ksqVSqBQChdt6OeAAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "274bb401552e398e-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 14 Feb 2016 21:00:32 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.5.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-02-22T05:34:53", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=refresh_token&refresh_token=" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "77" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "User-Agent": [ 30 | "prawcore:test (by /u/bboe) prawcore/1.5.0" 31 | ] 32 | }, 33 | "method": "POST", 34 | "uri": "https://www.reddit.com/api/v1/access_token" 35 | }, 36 | "response": { 37 | "body": { 38 | "encoding": "UTF-8", 39 | "string": "{\"access_token\": \"0000000-aaaaaaaaaaaaaaaaaaaaaa-0000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"refresh_token\": \"aaaaaaa-0000000000000000000000-aaaaaaa\", \"scope\": \"modmail\"}" 40 | }, 41 | "headers": { 42 | "Accept-Ranges": [ 43 | "bytes" 44 | ], 45 | "Connection": [ 46 | "close" 47 | ], 48 | "Content-Length": [ 49 | "181" 50 | ], 51 | "Content-Type": [ 52 | "application/json; charset=UTF-8" 53 | ], 54 | "Date": [ 55 | "Mon, 22 Feb 2021 05:34:53 GMT" 56 | ], 57 | "Server": [ 58 | "snooserv" 59 | ], 60 | "Set-Cookie": [ 61 | "edgebucket=f5KZ6I9GmO6zC9InB3; Domain=reddit.com; Max-Age=63071999; Path=/; secure" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Moose": [ 70 | "majestic" 71 | ], 72 | "cache-control": [ 73 | "max-age=0, must-revalidate" 74 | ], 75 | "x-content-type-options": [ 76 | "nosniff" 77 | ], 78 | "x-frame-options": [ 79 | "SAMEORIGIN" 80 | ], 81 | "x-xss-protection": [ 82 | "1; mode=block" 83 | ] 84 | }, 85 | "status": { 86 | "code": 200, 87 | "message": "OK" 88 | }, 89 | "url": "https://www.reddit.com/api/v1/access_token" 90 | } 91 | } 92 | ], 93 | "recorded_with": "betamax/0.8.1" 94 | } 95 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_refresh__with_invalid_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-07T01:02:52", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=refresh_token&refresh_token=INVALID_TOKEN" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "52", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "User-Agent": "prawcore/0.0.1a1" 18 | }, 19 | "method": "POST", 20 | "uri": "https://www.reddit.com/api/v1/access_token" 21 | }, 22 | "response": { 23 | "body": { 24 | "encoding": "UTF-8", 25 | "string": "{\"error\": 400}" 26 | }, 27 | "headers": { 28 | "CF-RAY": "270b2bfaced23982-PHX", 29 | "Connection": "keep-alive", 30 | "Content-Length": "14", 31 | "Content-Type": "application/json; charset=UTF-8", 32 | "Date": "Sun, 07 Feb 2016 01:02:52 GMT", 33 | "Server": "cloudflare-nginx", 34 | "Set-Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972; expires=Mon, 06-Feb-17 01:02:52 GMT; path=/; domain=.reddit.com; HttpOnly", 35 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 36 | "X-Moose": "majestic", 37 | "cache-control": "max-age=0, must-revalidate", 38 | "x-content-type-options": "nosniff", 39 | "x-frame-options": "SAMEORIGIN", 40 | "x-ua-compatible": "IE=edge", 41 | "x-xss-protection": "1; mode=block" 42 | }, 43 | "status": { 44 | "code": 400, 45 | "message": "Bad Request" 46 | }, 47 | "url": "https://www.reddit.com/api/v1/access_token" 48 | } 49 | } 50 | ], 51 | "recorded_with": "betamax/0.5.1" 52 | } 53 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestAuthorizer.test_revoke__refresh_token_without_access_set.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-07T09:31:09", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "token=&token_type_hint=refresh_token" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "54" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=whsHxMuMAvrj45AdCl; loid=0000000000cksuire2.2.1623058268671.Z0FBQUFBQmd2ZWRjZ3VXVUluMld4VXBuVFNpVUtmdUo5Qmlac0xqQmw4RnNaNzZkN3RGcW9SMzVDTWJTUW81OFVtME9rcW5yak5iX3djdXVDNDk5Znl1clZkdERSM1EyR2NFQUlKbUFldVlWSVhDRTRHVUpwT3cxTW1tSXhfbWUxRTFiWldBc29LaFo; session_tracker=whsHxMuMAvrj45AdCl.0.1623058269105.Z0FBQUFBQmd2ZWRjSWVNelA3U1Z6elpFUGJSc3l5X3ktZHlUdFRmbkhidGpFYmhOYnZ4UHlxRzdOM0p6a0dsOEFiY3NBcjBZN0JoMWc5TTBiU3Jmb1NHcFpTLTJtSEhiZjRELVdYb0tRMFNvNUFlU053SHZ2eUkwdGFZWjkzZ2s4WDJrZmxvcE96R20" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/revoke_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "0" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Mon, 07 Jun 2021 09:31:09 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "293" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "531" 89 | ], 90 | "x-ratelimit-used": [ 91 | "7" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/revoke_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestDeviceIDAuthorizer.test_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-08-06T15:26:46", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "device_id=DO_NOT_TRACK_THIS_DEVICE&grant_type=https%3A%2F%2Foauth.reddit.com%2Fgrants%2Finstalled_client" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic N3poaE9NdGNPQUlxU2c6", 14 | "Connection": "keep-alive", 15 | "Content-Length": "104", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.1.0" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUMp2zDFxqwgMdCnLdkwyDPDOzM/LdCxNjHCPKlfSUVACq4svqSxIBSlOSk0sSi0CiaeklmUmp8ZnpoCEXfzj/fxD4kOCHJ2940M8PIPjXVzDPJ1dQQpTKwoyi1KL4zNBthmbGRjoKCgVJ+dDzNNSqgUAMHH1LJIAAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2ce38254ea6613ef-LAX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 06 Aug 2016 15:26:46 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.7.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestDeviceIDAuthorizer.test_refresh__with_scopes_and_trusted_authenticator.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-25T11:21:08", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "device_id=DO_NOT_TRACK_THIS_DEVICE&grant_type=https%3A%2F%2Foauth.reddit.com%2Fgrants%2Finstalled_client&scope=adsedit+adsread+creddits+history" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "143" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=xabc3mKo7opLvCrF2B" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.2.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"access_token\": \"-000000000000000000000000000000\", \"token_type\": \"bearer\", \"device_id\": \"DO_NOT_TRACK_THIS_DEVICE\", \"expires_in\": 3600, \"scope\": \"adsedit adsread creddits history\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "181" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Fri, 25 Jun 2021 11:21:09 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "299" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "531" 89 | ], 90 | "x-ratelimit-used": [ 91 | "1" 92 | ], 93 | "x-reddit-loid": [ 94 | "0000000000cxgu11le.2.1624620069713.Z0FBQUFBQmcxYndseVQ0QzNwTHE4aWtDQmNpTEktZmFFQ19qVjBnc0JDcDVYbDdTaUc0eDU2dGxjWil5MHRnWE5YeUFlNDRWS3BDaDZjV3h3ckRJWWgwVWFjWnNqMzJWbUxEdEZDMVVzRlBUQ2lMcFwMTk1FTU94aXNGeUFuUfQ0UFllR3ZfaUZTZWw" 95 | ], 96 | "x-xss-protection": [ 97 | "1; mode=block" 98 | ] 99 | }, 100 | "status": { 101 | "code": 200, 102 | "message": "OK" 103 | }, 104 | "url": "https://www.reddit.com/api/v1/access_token" 105 | } 106 | } 107 | ], 108 | "recorded_with": "betamax/0.8.1" 109 | } 110 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestDeviceIDAuthorizer.test_refresh__with_short_device_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-08-06T15:26:46", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "device_id=aaaaaaaaaaaaaaaaaaa&grant_type=https%3A%2F%2Foauth.reddit.com%2Fgrants%2Finstalled_client" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic N3poaE9NdGNPQUlxU2c6", 14 | "Connection": "keep-alive", 15 | "Content-Length": "99", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.1.0" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSi0qyi+KT0ktTi7KLCjJzM9TslJQSkpMUUhJLctMTo3PTFHSUYCoAslk5pUl5mSmxBelFpamFpco1QIA5tKqikIAAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2ce382559a6e13ef-LAX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 06 Aug 2016 15:26:46 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.7.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestReadOnlyAuthorizer.test_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T00:50:20", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "29", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUNItzKpK13UPKvcwMIrMrjAvMKv09y4pzk8vSstX0lFQAiuML6ksSAWpTkpNLEotAomnVhRkFqUWx2eCTDE2MzDQUVAqTs6HKNNSqgUAJ2OjhWoAAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2744c73d666939d6-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 14 Feb 2016 00:50:20 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.5.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestReadOnlyAuthorizer.test_refresh__with_scopes.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-25T11:21:09", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials&scope=adsedit+adsread+creddits+history" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "68" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=r9opzEFW1Cgq4ECFIm" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.2.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"access_token\": \"-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"adsedit adsread creddits history\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "140" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Fri, 25 Jun 2021 11:21:10 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "298" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "531" 89 | ], 90 | "x-ratelimit-used": [ 91 | "2" 92 | ], 93 | "x-reddit-loid": [ 94 | "0000000000cxgu137i.2.1624620069996.Z0FBQUFBQmcxYndtV3lJNlpzTlhPdHA5NUQ5VWV3X0RlbV9hWkNuMmVnWWRhS0hZbzRwaGVRaFNT9FR5SmZMY3ZlSGhod1hXc0w3cEgwRGRnNXR5QmVhb2ZEYk9sMHdPWlAzLW5Oam1WRm1UYWJhWVFjUDeCd3NORXBxTjJtMW5lTWFUazZQeUZDQmw" 95 | ], 96 | "x-xss-protection": [ 97 | "1; mode=block" 98 | ] 99 | }, 100 | "status": { 101 | "code": 200, 102 | "message": "OK" 103 | }, 104 | "url": "https://www.reddit.com/api/v1/access_token" 105 | } 106 | } 107 | ], 108 | "recorded_with": "betamax/0.8.1" 109 | } 110 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestScriptAuthorizer.test_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T04:18:42", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXDXat8s02jy8odEtzDEtxqzTNqyzLSY4yDjGwUNJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjTUqoFAEFYUoFxAAAA", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2745f875e9c939ca-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 14 Feb 2016 04:18:42 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.5.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestScriptAuthorizer.test_refresh__with_invalid_otp.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-05-21T00:48:30", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&otp=fake&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "152" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=7C1qdPEdiInzFy9WaL; loid=bSusfEGk5JXrZYhhyW" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"error\": \"invalid_grant\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "26" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Fri, 21 May 2021 00:48:33 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Moose": [ 70 | "majestic" 71 | ], 72 | "cache-control": [ 73 | "max-age=0, must-revalidate" 74 | ], 75 | "x-content-type-options": [ 76 | "nosniff" 77 | ], 78 | "x-frame-options": [ 79 | "SAMEORIGIN" 80 | ], 81 | "x-ratelimit-remaining": [ 82 | "297" 83 | ], 84 | "x-ratelimit-reset": [ 85 | "87" 86 | ], 87 | "x-ratelimit-used": [ 88 | "3" 89 | ], 90 | "x-xss-protection": [ 91 | "1; mode=block" 92 | ] 93 | }, 94 | "status": { 95 | "code": 200, 96 | "message": "OK" 97 | }, 98 | "url": "https://www.reddit.com/api/v1/access_token" 99 | } 100 | } 101 | ], 102 | "recorded_with": "betamax/0.8.1" 103 | } 104 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestScriptAuthorizer.test_refresh__with_invalid_username_or_password.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-14T04:14:13", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=invalidpassword&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "67", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAAyXLUQqAIAwA0KuMfXeCLtERZOggwdzYlLDo7iH+PngvsplYSOzRsrYsFXfAo5YBS4BUHS4a0J1Byf0WS0C9nbjB6rP0OkksP5xCLJlrw+8Hb2QyDF8AAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2745f1e804c539ac-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 14 Feb 2016 04:14:13 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | } 51 | ], 52 | "recorded_with": "betamax/0.5.1" 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestScriptAuthorizer.test_refresh__with_scopes.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-25T11:21:09", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&scope=adsedit+adsread+creddits+history&username=" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "191" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=Q9QMMTymjQTL5E83Aa" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.2.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"access_token\": \"12345678-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"adsedit adsread creddits history\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "147" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Fri, 25 Jun 2021 11:21:10 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "297" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "530" 89 | ], 90 | "x-ratelimit-used": [ 91 | "3" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/access_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestScriptAuthorizer.test_refresh__with_valid_otp.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-05-21T00:43:11", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&otp=000000&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "152" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=W5X0f17fr5uGzS0Jxd; loid=yU0JIU6fMP2ZtL3FsU" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"access_token\": \"00000000-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "117" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Fri, 21 May 2021 00:43:14 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Moose": [ 70 | "majestic" 71 | ], 72 | "cache-control": [ 73 | "max-age=0, must-revalidate" 74 | ], 75 | "x-content-type-options": [ 76 | "nosniff" 77 | ], 78 | "x-frame-options": [ 79 | "SAMEORIGIN" 80 | ], 81 | "x-ratelimit-remaining": [ 82 | "299" 83 | ], 84 | "x-ratelimit-reset": [ 85 | "406" 86 | ], 87 | "x-ratelimit-used": [ 88 | "1" 89 | ], 90 | "x-xss-protection": [ 91 | "1; mode=block" 92 | ] 93 | }, 94 | "status": { 95 | "code": 200, 96 | "message": "OK" 97 | }, 98 | "url": "https://www.reddit.com/api/v1/access_token" 99 | } 100 | } 101 | ], 102 | "recorded_with": "betamax/0.8.1" 103 | } 104 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__accepted.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-09T23:00:18", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "152" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "User-Agent": [ 30 | "prawcore:test (by /u/bboe) prawcore/2.1.0" 31 | ] 32 | }, 33 | "method": "POST", 34 | "uri": "https://www.reddit.com/api/v1/access_token" 35 | }, 36 | "response": { 37 | "body": { 38 | "encoding": "UTF-8", 39 | "string": "{\"access_token\": \"00000000-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" 40 | }, 41 | "headers": { 42 | "Accept-Ranges": [ 43 | "bytes" 44 | ], 45 | "Connection": [ 46 | "close" 47 | ], 48 | "Content-Length": [ 49 | "117" 50 | ], 51 | "Content-Type": [ 52 | "application/json; charset=UTF-8" 53 | ], 54 | "Date": [ 55 | "Wed, 09 Jun 2021 23:00:17 GMT" 56 | ], 57 | "Server": [ 58 | "snooserv" 59 | ], 60 | "Set-Cookie": [ 61 | "edgebucket=KLG8DuWl7lo6rq6Dzq; Domain=reddit.com; Max-Age=63071999; Path=/; secure" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "299" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "583" 89 | ], 90 | "x-ratelimit-used": [ 91 | "1" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/access_token" 102 | } 103 | }, 104 | { 105 | "recorded_at": "2021-06-09T23:00:18", 106 | "request": { 107 | "body": { 108 | "encoding": "utf-8", 109 | "string": "" 110 | }, 111 | "headers": { 112 | "Accept": [ 113 | "*/*" 114 | ], 115 | "Accept-Encoding": [ 116 | "gzip, deflate" 117 | ], 118 | "Authorization": [ 119 | "bearer 00000000-000000000000000000000000000000" 120 | ], 121 | "Connection": [ 122 | "keep-alive" 123 | ], 124 | "Content-Length": [ 125 | "0" 126 | ], 127 | "Cookie": [ 128 | "edgebucket=KLG8DuWl7lo6rq6Dzq" 129 | ], 130 | "User-Agent": [ 131 | "prawcore:test (by /u/bboe) prawcore/2.1.0" 132 | ] 133 | }, 134 | "method": "POST", 135 | "uri": "https://oauth.reddit.com/api/read_all_messages?raw_json=1" 136 | }, 137 | "response": { 138 | "body": { 139 | "encoding": "UTF-8", 140 | "string": "{}" 141 | }, 142 | "headers": { 143 | "Accept-Ranges": [ 144 | "bytes" 145 | ], 146 | "Connection": [ 147 | "keep-alive" 148 | ], 149 | "Content-Length": [ 150 | "2" 151 | ], 152 | "Content-Type": [ 153 | "application/json; charset=UTF-8" 154 | ], 155 | "Date": [ 156 | "Wed, 09 Jun 2021 23:00:18 GMT" 157 | ], 158 | "Server": [ 159 | "snooserv" 160 | ], 161 | "Strict-Transport-Security": [ 162 | "max-age=15552000; includeSubDomains; preload" 163 | ], 164 | "Via": [ 165 | "1.1 varnish" 166 | ], 167 | "X-Clacks-Overhead": [ 168 | "GNU Terry Pratchett" 169 | ], 170 | "X-Moose": [ 171 | "majestic" 172 | ], 173 | "cache-control": [ 174 | "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate" 175 | ], 176 | "expires": [ 177 | "-1" 178 | ], 179 | "set-cookie": [ 180 | "redesign_optout=true; Domain=reddit.com; Max-Age=94607999; Path=/; expires=Sat, 08-Jun-2024 23:00:18 GMT; secure", 181 | "session_tracker=6GEhiIYy1IKboYp6F8.0.1623279618070.Z0FBQUFBQmd3VWdDcW5WVsk0UE5Dc2JJME1QaG5ncDdfTDgtOTRtWWxxLXJmWWFsS0NaaWdrvzRkQmJnbEhIUkYzcm9FNGlHN1p6N3ZoODM3cTliWDRseFNwTDBYSHNdWE15Wkhvc1VCVW1zUU81cVFmYy1wbW5tRG9aUkU4Q1FlQTBTemdRSTA2Y0s; Domain=reddit.com; Max-Age=7199; Path=/; expires=Thu, 10-Jun-2021 01:00:18 GMT; secure" 182 | ], 183 | "x-content-type-options": [ 184 | "nosniff" 185 | ], 186 | "x-frame-options": [ 187 | "SAMEORIGIN" 188 | ], 189 | "x-ratelimit-remaining": [ 190 | "599.0" 191 | ], 192 | "x-ratelimit-reset": [ 193 | "582" 194 | ], 195 | "x-ratelimit-used": [ 196 | "1" 197 | ], 198 | "x-ua-compatible": [ 199 | "IE=edge" 200 | ], 201 | "x-xss-protection": [ 202 | "1; mode=block" 203 | ] 204 | }, 205 | "status": { 206 | "code": 202, 207 | "message": "Accepted" 208 | }, 209 | "url": "https://oauth.reddit.com/api/read_all_messages?raw_json=1" 210 | } 211 | } 212 | ], 213 | "recorded_with": "betamax/0.8.1" 214 | } 215 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__bad_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2017-08-25T22:48:16", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "60", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.11.0" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "encoding": "UTF-8", 26 | "string": "{\"access_token\": \"rV4dzC_TbDFORiidV8igRY6sdTg\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" 27 | }, 28 | "headers": { 29 | "Accept-Ranges": "bytes", 30 | "Connection": "keep-alive", 31 | "Content-Length": "105", 32 | "Content-Type": "application/json; charset=UTF-8", 33 | "Date": "Fri, 25 Aug 2017 22:48:15 GMT", 34 | "Server": "snooserv", 35 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 36 | "Via": "1.1 varnish", 37 | "X-Cache": "MISS", 38 | "X-Cache-Hits": "0", 39 | "X-Moose": "majestic", 40 | "X-Served-By": "cache-lax8631-LAX", 41 | "X-Timer": "S1503701296.574156,VS0,VE423", 42 | "cache-control": "max-age=0, must-revalidate", 43 | "set-cookie": "session_tracker=5I4qTKz939kybjxETG.0.1503701295609.Z0FBQUFBQlpvS2t2MjF1eFVTaG5tWU05Nl9LMU0zaFR2OTVMRGN2WF9ob3YyZ1hfNWtaci1zZVJFNUh4VGRLclRHVjVRSERnWTVUcXE1OTdKdkJHQmVDY0NMRkpZY01NbjZkTVY1UUpWbU1hdEdBa1NQYWJNUFEzX194S1EwTDctellsblBXckZWV0o; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sat, 26-Aug-2017 00:48:15 GMT; secure", 44 | "x-content-type-options": "nosniff", 45 | "x-frame-options": "SAMEORIGIN", 46 | "x-xss-protection": "1; mode=block" 47 | }, 48 | "status": { 49 | "code": 200, 50 | "message": "OK" 51 | }, 52 | "url": "https://www.reddit.com/api/v1/access_token" 53 | } 54 | }, 55 | { 56 | "recorded_at": "2017-08-25T22:48:16", 57 | "request": { 58 | "body": { 59 | "encoding": "utf-8", 60 | "string": "" 61 | }, 62 | "headers": { 63 | "Accept": "*/*", 64 | "Accept-Encoding": "gzip, deflate", 65 | "Authorization": "bearer rV4dzC_TbDFORiidV8igRY6sdTg", 66 | "Connection": "keep-alive", 67 | "Cookie": "edgebucket=2U1nETSEgkF7NLH4Ek; loid=S1UX7gEWPLHZFXDMKZ; session_tracker=5I4qTKz939kybjxETG.0.1503701295609.Z0FBQUFBQlpvS2t2MjF1eFVTaG5tWU05Nl9LMU0zaFR2OTVMRGN2WF9ob3YyZ1hfNWtaci1zZVJFNUh4VGRLclRHVjVRSERnWTVUcXE1OTdKdkJHQmVDY0NMRkpZY01NbjZkTVY1UUpWbU1hdEdBa1NQYWJNUFEzX194S1EwTDctellsblBXckZWV0o", 68 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.11.0" 69 | }, 70 | "method": "GET", 71 | "uri": "https://oauth.reddit.com/?raw_json=1" 72 | }, 73 | "response": { 74 | "body": { 75 | "encoding": "UTF-8", 76 | "string": "{\"kind\": \"Listing\", \"data\": {\"modhash\": null, \"children\": [], \"after\": null, \"before\": null}" 77 | }, 78 | "headers": { 79 | "Accept-Ranges": "bytes", 80 | "Connection": "keep-alive", 81 | "Content-Length": "92", 82 | "Content-Type": "application/json; charset=UTF-8", 83 | "Date": "Fri, 25 Aug 2017 22:48:16 GMT", 84 | "Server": "snooserv", 85 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 86 | "Via": "1.1 varnish", 87 | "X-Cache": "MISS", 88 | "X-Cache-Hits": "0", 89 | "X-Moose": "majestic", 90 | "X-Served-By": "cache-lax8649-LAX", 91 | "X-Timer": "S1503701296.082092,VS0,VE387", 92 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate, max-age=0, must-revalidate", 93 | "expires": "-1", 94 | "set-cookie": "loid=00000000000004cixf.2.1284611485984.Z0FBQUFBQlpvS2t3TmxzOWZWaUlEeDJiOEI4RU55VnF3Q281WklZR2NUQ0J2blNUd1YwVjdxeUMyVGRwR0ZXUURraW50ZVlGUEF4N05paTZqYzZBb2NNWUN0TnJmVFFaWFMyaWxYTzZnZ1VMbDJLQTlveW5taWFUVGE5TXZGbXA3S1BHYzNNRGlMMTg; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Sun, 25-Aug-2019 22:48:16 GMT; secure", 95 | "x-content-type-options": "nosniff", 96 | "x-frame-options": "SAMEORIGIN", 97 | "x-ratelimit-remaining": "598.0", 98 | "x-ratelimit-reset": "104", 99 | "x-ratelimit-used": "2", 100 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=G12qA17RQEaZmlD9m5up%2FQTUV8iBY0RQ1PdYFotL%2FRuxnkGrhGyqJo2OO3kWZPIDoH0wpVxPs5D8RwFK8idQ%2FGj1TlepdKHe", 101 | "x-ua-compatible": "IE=edge", 102 | "x-xss-protection": "1; mode=block" 103 | }, 104 | "status": { 105 | "code": 200, 106 | "message": "OK" 107 | }, 108 | "url": "https://oauth.reddit.com/?raw_json=1" 109 | } 110 | } 111 | ], 112 | "recorded_with": "betamax/0.8.0" 113 | } 114 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__bad_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-09T23:09:27", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUMqKTLKsTEoPC/Ay9S9ON3V0SUvPyDQIs0gsilTSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQCMctsQaQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2bff7190c8c9297b-DUB", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 09 Jul 2016 23:09:27 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-09T23:09:27", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "{\"note\": \"prawcore\"}" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer jYb9ybgVPJ5Osg5ADfghi0V8arY", 62 | "Connection": "keep-alive", 63 | "Content-Length": "20", 64 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 65 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 66 | }, 67 | "method": "PUT", 68 | "uri": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 69 | }, 70 | "response": { 71 | "body": { 72 | "encoding": "UTF-8", 73 | "string": "{\"fields\": [\"note\"], \"explanation\": \"you must have an active reddit gold subscription to do that\", \"message\": \"Bad Request\", \"reason\": \"GOLD_REQUIRED\"}" 74 | }, 75 | "headers": { 76 | "CF-RAY": "2bff719571a6299f-DUB", 77 | "Connection": "keep-alive", 78 | "Content-Length": "151", 79 | "Content-Type": "application/json; charset=UTF-8", 80 | "Date": "Sat, 09 Jul 2016 23:09:27 GMT", 81 | "Server": "cloudflare-nginx", 82 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 83 | "X-Moose": "majestic", 84 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 85 | "expires": "-1", 86 | "x-content-type-options": "nosniff", 87 | "x-frame-options": "SAMEORIGIN", 88 | "x-ratelimit-remaining": "597.0", 89 | "x-ratelimit-reset": "33", 90 | "x-ratelimit-used": "3", 91 | "x-ua-compatible": "IE=edge", 92 | "x-xss-protection": "1; mode=block" 93 | }, 94 | "status": { 95 | "code": 400, 96 | "message": "Bad Request" 97 | }, 98 | "url": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 99 | } 100 | } 101 | ], 102 | "recorded_with": "betamax/0.7.1" 103 | } 104 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2017-05-28T00:44:53", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.10.1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "encoding": "UTF-8", 26 | "string": "{\"access_token\": \"B766tuE2hYKwrldCkYF0U3raaVQ\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" 27 | }, 28 | "headers": { 29 | "Accept-Ranges": "bytes", 30 | "Connection": "keep-alive", 31 | "Content-Length": "105", 32 | "Content-Type": "application/json; charset=UTF-8", 33 | "Date": "Sun, 28 May 2017 00:44:55 GMT", 34 | "Server": "snooserv", 35 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 36 | "Via": "1.1 varnish", 37 | "X-Cache": "MISS", 38 | "X-Cache-Hits": "0", 39 | "X-Moose": "majestic", 40 | "X-Served-By": "cache-ord1726-ORD", 41 | "X-Timer": "S1495932295.828444,VS0,VE356", 42 | "cache-control": "max-age=0, must-revalidate", 43 | "set-cookie": "session_tracker=DJojrrAVPFxgb0vizI.0.1495932294853.Z0FBQUFBQlpLaDJIaFl0OUFYbXdtejdaSmg3Mm5ZZ19EZ2JzTzZhVlFEbERZUVZ2RzJuOUpVWkUwQVBQUmRBLWpZWUVEaHBzNy1nTEh1LXN3TVZHa1dwNDJ1ai05cko1OWhWRDVFLWw2MEN5VEdmNTdHejZUcFJwWUhiUWtmSS1nY2czaklKanNoTVY; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sun, 28-May-2017 02:44:55 GMT; secure", 44 | "x-content-type-options": "nosniff", 45 | "x-frame-options": "SAMEORIGIN", 46 | "x-xss-protection": "1; mode=block" 47 | }, 48 | "status": { 49 | "code": 200, 50 | "message": "OK" 51 | }, 52 | "url": "https://www.reddit.com/api/v1/access_token" 53 | } 54 | }, 55 | { 56 | "recorded_at": "2017-05-28T00:44:54", 57 | "request": { 58 | "body": { 59 | "encoding": "utf-8", 60 | "string": "api_type=json&content=New+text&page=index&previous=f0214574-430d-11e7-84ca-1201093304fa" 61 | }, 62 | "headers": { 63 | "Accept": "*/*", 64 | "Accept-Encoding": "gzip, deflate", 65 | "Authorization": "bearer B766tuE2hYKwrldCkYF0U3raaVQ", 66 | "Connection": "keep-alive", 67 | "Content-Length": "87", 68 | "Content-Type": "application/x-www-form-urlencoded", 69 | "Cookie": "edgebucket=NTUmIaQTzkbc6Ni4JG; loid=S1UX7gEWPLHZFXDMKZ; session_tracker=DJojrrAVPFxgb0vizI.0.1495932294853.Z0FBQUFBQlpLaDJIaFl0OUFYbXdtejdaSmg3Mm5ZZ19EZ2JzTzZhVlFEbERZUVZ2RzJuOUpVWkUwQVBQUmRBLWpZWUVEaHBzNy1nTEh1LXN3TVZHa1dwNDJ1ai05cko1OWhWRDVFLWw2MEN5VEdmNTdHejZUcFJwWUhiUWtmSS1nY2czaklKanNoTVY", 70 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.10.1" 71 | }, 72 | "method": "POST", 73 | "uri": "https://oauth.reddit.com/r/ThirdRealm/api/wiki/edit?raw_json=1" 74 | }, 75 | "response": { 76 | "body": { 77 | "base64_string": "H4sIAIcdKlkC/81V0UrDMBT9lVJfHU276VDxqU4YyHzxsVDS5HYtdElJUucQ/92kaVnTWUTQYR9GdnPuuSfn3qbvvgAsOfNvPX/1sH5J4+fN49M6fvEvPZ+WeU44U8CU2U+Yp5+kQWgeK5xV4JEKS3mftMDE90rarasyS0mxTRVHqf6pky63ewhUlawxKdlWJyCdaSI1pvQYEU0FhnoreFNLHWjrro48VgfhVYvodttY4Ab/AvlPZagCMB3ilXD+FU7HUgZvSrdmgMiEFzj1VDEm4KZ1TFNEpm9DOlMcRE9IGiH04HhASzVJdjY1B96IaSmB61Mw9HFsccbpYbw12HYNp18dceI9Qe7hsVcIyDXw4gRp36gW258Hu/rdIRip6J2xOnLBdygNe8JwioLxvcC1TrALV6xpgltENlkPyTnPsPUlDgzyB1pPh+JcvhjGX3dFX3E9ZAN7s4iuWSbrO2XOaWt849BxVkeTGZyMZh83d7WNm1t9B1LiLZgbPeZMO0iUCTPYC3gtZWk/BktEECIhni3mCGZhCMvZTU6vZghgGdEFIIiiLm3wibCt9j8+Ad8CkydXBgAA", 78 | "encoding": "UTF-8", 79 | "string": "" 80 | }, 81 | "headers": { 82 | "Accept-Ranges": "bytes", 83 | "Connection": "keep-alive", 84 | "Content-Encoding": "gzip", 85 | "Content-Length": "441", 86 | "Content-Type": "application/json; charset=UTF-8", 87 | "Date": "Sun, 28 May 2017 00:44:55 GMT", 88 | "Server": "snooserv", 89 | "Vary": "accept-encoding", 90 | "Via": "1.1 varnish", 91 | "X-Cache": "MISS", 92 | "X-Cache-Hits": "0", 93 | "X-Moose": "majestic", 94 | "X-Served-By": "cache-ord1747-ORD", 95 | "X-Timer": "S1495932295.433097,VS0,VE97", 96 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate, max-age=0, must-revalidate", 97 | "expires": "-1", 98 | "set-cookie": "loid=0000000000000xw27h.2.1463097178233.Z0FBQUFBQlpLaDJIUjNpb2xwbFAycjQ2N3lSOExiMG5OTnFMZmNYenJ0X0pCaEZDcjFOLWZvaDlVOG9lRlppVFZQTlc1SWFwX3dlZTMwcks5R3pZclVOYVRzMXd2bnZSNXpGc0I3WElraVZreU1NT2FPdDhvVVZDOGtiTk0xcDFlakphY1E1d3o0QXk; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Tue, 28-May-2019 00:44:55 GMT; secure", 99 | "x-content-type-options": "nosniff", 100 | "x-frame-options": "SAMEORIGIN", 101 | "x-ratelimit-remaining": "598.0", 102 | "x-ratelimit-reset": "305", 103 | "x-ratelimit-used": "2", 104 | "x-ua-compatible": "IE=edge", 105 | "x-xss-protection": "1; mode=block" 106 | }, 107 | "status": { 108 | "code": 409, 109 | "message": "Conflict" 110 | }, 111 | "url": "https://oauth.reddit.com/r/ThirdRealm/api/wiki/edit?raw_json=1" 112 | } 113 | } 114 | ], 115 | "recorded_with": "betamax/0.8.0" 116 | } 117 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__created.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-09T22:35:39", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.8" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUCorDI7PzshNtPSOqAxPzTEpTSxzdzI3D8zzN1HSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQAplHoZaQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2bff40116d902993-DUB", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 09 Jul 2016 22:35:39 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-09T22:35:40", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "{}" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer vqS_khma9KXyWel4uavGB77QnO4", 62 | "Connection": "keep-alive", 63 | "Content-Length": "2", 64 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 65 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.8" 66 | }, 67 | "method": "PUT", 68 | "uri": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 69 | }, 70 | "response": { 71 | "body": { 72 | "encoding": "UTF-8", 73 | "string": "{\"date\": 1468103740.0, \"name\": \"spez\", \"id\": \"t2_1w72\"}" 74 | }, 75 | "headers": { 76 | "CF-RAY": "2bff401854782993-DUB", 77 | "Connection": "keep-alive", 78 | "Content-Length": "55", 79 | "Content-Type": "application/json; charset=UTF-8", 80 | "Date": "Sat, 09 Jul 2016 22:35:40 GMT", 81 | "Server": "cloudflare-nginx", 82 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 83 | "X-Moose": "majestic", 84 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 85 | "expires": "-1", 86 | "x-content-type-options": "nosniff", 87 | "x-frame-options": "SAMEORIGIN", 88 | "x-ratelimit-remaining": "599.0", 89 | "x-ratelimit-reset": "260", 90 | "x-ratelimit-used": "1", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 201, 95 | "message": "Created" 96 | }, 97 | "url": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.7.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__forbidden.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-03-22T02:18:37", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.8" 18 | }, 19 | "method": "POST", 20 | "uri": "https://www.reddit.com/api/v1/access_token" 21 | }, 22 | "response": { 23 | "body": { 24 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXjYzyjo8sCc30zE41D/IxNshLzHJLdHH1qUhNV9JRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjTUqoFAKNHbOtxAAAA", 25 | "encoding": "UTF-8", 26 | "string": "" 27 | }, 28 | "headers": { 29 | "CF-RAY": "2876276d91e139ee-PHX", 30 | "Connection": "keep-alive", 31 | "Content-Encoding": "gzip", 32 | "Content-Type": "application/json; charset=UTF-8", 33 | "Date": "Tue, 22 Mar 2016 02:18:37 GMT", 34 | "Server": "cloudflare-nginx", 35 | "Set-Cookie": "__cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117; expires=Wed, 22-Mar-17 02:18:37 GMT; path=/; domain=.reddit.com; HttpOnly", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-03-22T02:18:38", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer 7302867-YZK_YtUiIke7RL30najFaDELxeg", 62 | "Connection": "keep-alive", 63 | "Cookie": "loid=5ja5onM1PufSns9XKR; loidcreated=2016-03-22T02%3A18%3A37.126Z; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 64 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.8" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/user/spez/gilded/given?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "encoding": "UTF-8", 72 | "string": "{\"error\": 403}" 73 | }, 74 | "headers": { 75 | "CF-RAY": "28762776009a3988-PHX", 76 | "Connection": "keep-alive", 77 | "Content-Length": "14", 78 | "Content-Type": "application/json; charset=UTF-8", 79 | "Date": "Tue, 22 Mar 2016 02:18:38 GMT", 80 | "Server": "cloudflare-nginx", 81 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 82 | "X-Moose": "majestic", 83 | "access-control-allow-origin": "*", 84 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 85 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 86 | "expires": "-1", 87 | "x-content-type-options": "nosniff", 88 | "x-frame-options": "SAMEORIGIN", 89 | "x-ratelimit-remaining": "599.0", 90 | "x-ratelimit-reset": "82", 91 | "x-ratelimit-used": "1", 92 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=LeF3GiZkLAxxG193GpzG2BpDSvZi%2FfC58vhP4bmdK2uNq6qLGv3kOjAklpSOQCDdmJY7CMNNZCbPA51aRHn0eqlwBgPbSN8u", 93 | "x-ua-compatible": "IE=edge", 94 | "x-xss-protection": "1; mode=block" 95 | }, 96 | "status": { 97 | "code": 403, 98 | "message": "Forbidden" 99 | }, 100 | "url": "https://oauth.reddit.com/user/spez/gilded/given?raw_json=1" 101 | } 102 | } 103 | ], 104 | "recorded_with": "betamax/0.5.1" 105 | } 106 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__no_content.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-09T23:02:18", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUMopjPSKNDVLqkpPyfVwsihKNzbJ8DZzLTZJS1fSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQCnwDS+aQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2bff671aee2f2969-DUB", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 09 Jul 2016 23:02:18 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-09T23:02:18", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer lqYJY56bzgdmHB8rg34hK6Es4fg", 62 | "Connection": "keep-alive", 63 | "Content-Length": "0", 64 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 65 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 66 | }, 67 | "method": "DELETE", 68 | "uri": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 69 | }, 70 | "response": { 71 | "body": { 72 | "encoding": "UTF-8", 73 | "string": "" 74 | }, 75 | "headers": { 76 | "CF-RAY": "2bff671f9154296f-DUB", 77 | "Connection": "keep-alive", 78 | "Content-Length": "0", 79 | "Content-Type": "application/json; charset=UTF-8", 80 | "Date": "Sat, 09 Jul 2016 23:02:18 GMT", 81 | "Server": "cloudflare-nginx", 82 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 83 | "X-Moose": "majestic", 84 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 85 | "expires": "-1", 86 | "x-content-type-options": "nosniff", 87 | "x-frame-options": "SAMEORIGIN", 88 | "x-ratelimit-remaining": "598.0", 89 | "x-ratelimit-reset": "462", 90 | "x-ratelimit-used": "2", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 204, 95 | "message": "No Content" 96 | }, 97 | "url": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.7.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-16T23:18:17", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.10" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUMorqCor9qky8g6szAgrDioydUt2dg0xsExzdVTSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQCG2wYDaQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2c392c23fd2420ea-LAX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 16 Jul 2016 23:18:17 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-16T23:18:17", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer npzvsLz2KQyhVsRr5FcCET09fEA", 62 | "Connection": "keep-alive", 63 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 64 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.10" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/r/reddit_api_test/wiki/invalid?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "base64_string": "H4sIAAAAAAAAA6tWKkpNLM7PU7JSUApwdHeN9/MPiXcOcnUMcXVR0lFQyk0tLk5MTwVJ++WXKLjll+alKNUCAGwqHis2AAAA", 72 | "encoding": "UTF-8", 73 | "string": "" 74 | }, 75 | "headers": { 76 | "CF-RAY": "2c392c279b081419-LAX", 77 | "Connection": "keep-alive", 78 | "Content-Encoding": "gzip", 79 | "Content-Type": "application/json; charset=UTF-8", 80 | "Date": "Sat, 16 Jul 2016 23:18:17 GMT", 81 | "Server": "cloudflare-nginx", 82 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 83 | "Transfer-Encoding": "chunked", 84 | "X-Moose": "majestic", 85 | "access-control-allow-origin": "*", 86 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 87 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 88 | "expires": "-1", 89 | "x-content-type-options": "nosniff", 90 | "x-frame-options": "SAMEORIGIN", 91 | "x-ratelimit-remaining": "599.0", 92 | "x-ratelimit-reset": "103", 93 | "x-ratelimit-used": "1", 94 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=FUHn7OKOBkoMr4H%2BbW3OoYsk2SkAOUlF5k%2Bk6MGvWbGLikl1aYC9x0HZ%2BL%2BH0VpdtKy95gHNPJwHg10VuzSj1FFn7704V2PN", 95 | "x-ua-compatible": "IE=edge", 96 | "x-xss-protection": "1; mode=block" 97 | }, 98 | "status": { 99 | "code": 404, 100 | "message": "Not Found" 101 | }, 102 | "url": "https://oauth.reddit.com/r/reddit_api_test/wiki/invalid?raw_json=1" 103 | } 104 | } 105 | ], 106 | "recorded_with": "betamax/0.7.1" 107 | } 108 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__okay_with_0_byte_content.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-17T13:46:21", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.11" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUApJcyzP9bKILHExM8v2ynYqTw4JjCgONIt0tVDSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQBeYxEDaQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2c3e23b6c6c50673-LAX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 17 Jul 2016 13:46:21 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-17T13:46:21", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "api_type=json&model=%7B%22name%22%3A+%22redditdev%22%7D" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer TfAwmJ8YtD66kJkBwcTQXsQ6YE8", 62 | "Connection": "keep-alive", 63 | "Content-Length": "55", 64 | "Content-Type": "application/x-www-form-urlencoded", 65 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 66 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.11" 67 | }, 68 | "method": "DELETE", 69 | "uri": "https://oauth.reddit.com/api/multi/user//m/praw_x5g968f66a/r/redditdev?raw_json=1" 70 | }, 71 | "response": { 72 | "body": { 73 | "encoding": "UTF-8", 74 | "string": "" 75 | }, 76 | "headers": { 77 | "CF-RAY": "2c3e23bb13be22e2-LAX", 78 | "Connection": "keep-alive", 79 | "Content-Length": "0", 80 | "Content-Type": "application/json; charset=UTF-8", 81 | "Date": "Sun, 17 Jul 2016 13:46:21 GMT", 82 | "Server": "cloudflare-nginx", 83 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 84 | "X-Moose": "majestic", 85 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 86 | "expires": "-1", 87 | "x-content-type-options": "nosniff", 88 | "x-frame-options": "SAMEORIGIN", 89 | "x-ratelimit-remaining": "598.0", 90 | "x-ratelimit-reset": "219", 91 | "x-ratelimit-used": "2", 92 | "x-ua-compatible": "IE=edge", 93 | "x-xss-protection": "1; mode=block" 94 | }, 95 | "status": { 96 | "code": 200, 97 | "message": "OK" 98 | }, 99 | "url": "https://oauth.reddit.com/api/multi/user//m/praw_x5g968f66a/r/redditdev?raw_json=1" 100 | } 101 | } 102 | ], 103 | "recorded_with": "betamax/0.7.1" 104 | } 105 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__post.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-03-19T04:34:19", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=ol4hwS2uWoHYPNaM8g; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.6" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXzcoKLK+Icgo2TA53NKt00s10ytONL/UxNoyyUNJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjTUqoFAIzgL/txAAAA", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "285e36134fb239c4-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 19 Mar 2016 04:34:19 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-03-19T04:34:19", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "api_type=json&kind=self&sr=reddit_api_test&text=Test%21&title=A+Test+from+PRAWCORE." 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer 7302867-jjQwxZBS1cWA6yB-iBn-_uL31Z8", 62 | "Connection": "keep-alive", 63 | "Content-Length": "83", 64 | "Content-Type": "application/x-www-form-urlencoded", 65 | "Cookie": "loid=ol4hwS2uWoHYPNaM8g; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 66 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.6" 67 | }, 68 | "method": "POST", 69 | "uri": "https://oauth.reddit.com/api/submit?raw_json=1" 70 | }, 71 | "response": { 72 | "body": { 73 | "base64_string": "H4sIAAAAAAAAAy3MQQoDIQyF4atI1sOE0q68SimSqkMtjUpMcTF49+JMl//H4+3wbiWDNTtEkSINrLk/FgOBlA7+ygesgZdqbRax975KDCHp6guj4BmOanIam6IvzDFrw9vzohsjHew2KeyqUPdFIsJiIIX5e65mZ+I4Ra/uj2OMH9cEaiKgAAAA", 74 | "encoding": "UTF-8", 75 | "string": "" 76 | }, 77 | "headers": { 78 | "CF-RAY": "285e3616d03939d0-PHX", 79 | "Connection": "keep-alive", 80 | "Content-Encoding": "gzip", 81 | "Content-Type": "application/json; charset=UTF-8", 82 | "Date": "Sat, 19 Mar 2016 04:34:19 GMT", 83 | "Server": "cloudflare-nginx", 84 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 85 | "Transfer-Encoding": "chunked", 86 | "X-Moose": "majestic", 87 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 88 | "expires": "-1", 89 | "x-content-type-options": "nosniff", 90 | "x-frame-options": "SAMEORIGIN", 91 | "x-ratelimit-remaining": "599.0", 92 | "x-ratelimit-reset": "341", 93 | "x-ratelimit-used": "1", 94 | "x-ua-compatible": "IE=edge", 95 | "x-xss-protection": "1; mode=block" 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://oauth.reddit.com/api/submit?raw_json=1" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.5.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__raw_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-16T01:54:10", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "29", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "User-Agent": "prawcore/0.0.1a1" 18 | }, 19 | "method": "POST", 20 | "uri": "https://www.reddit.com/api/v1/access_token" 21 | }, 22 | "response": { 23 | "body": { 24 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUNJ1MzUOsUws8ja1LEx1Mgwrzjb1sPBMDY70NM5W0lFQAiuML6ksSAWpTkpNLEotAomnVhRkFqUWx2eCTDE2MzDQUVAqTs6HKNNSqgUAclclyGoAAAA=", 25 | "encoding": "UTF-8", 26 | "string": "" 27 | }, 28 | "headers": { 29 | "CF-RAY": "27559f8148e139ee-PHX", 30 | "Connection": "keep-alive", 31 | "Content-Encoding": "gzip", 32 | "Content-Type": "application/json; charset=UTF-8", 33 | "Date": "Tue, 16 Feb 2016 01:54:10 GMT", 34 | "Server": "cloudflare-nginx", 35 | "Set-Cookie": "__cfduid=d696d0c07d476cd15a974d5a0b6cf29741455587650; expires=Wed, 15-Feb-17 01:54:10 GMT; path=/; domain=.reddit.com; HttpOnly", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-02-16T01:54:11", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer -F53T9arK59qeB1Vsk5H8IeSYI3k", 62 | "Connection": "keep-alive", 63 | "Cookie": "__cfduid=d696d0c07d476cd15a974d5a0b6cf29741455587650", 64 | "User-Agent": "prawcore/0.0.1a1" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/r/reddit_api_test/comments/45xjdr/want_raw_json_test/?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "base64_string": "H4sIAEOBwlYC/51UyW7bMBD9FUPnIPISualvRtFDiyIN2hQ5OAFBiZTFmIvKxU5g+N87Q0mRpW5ATgLfPM7y5lGbY7ITmiWrSfJFOC/0NrmYJIx6CtAxUYZV1FUYRryohGSWazhv+ot+MbjDjKICKYnjsry0nDHhCa0F8dx5pOZUa85I/gIkHaQESHEmKOEq55jyeALIhby5i6n+kAWze/7sSeWV7DN1cNezFDvuzsJhu4UEUN4Zi6QWD45bYnkNILI3jzFVESwnsbeeKYXekVJSYUlbpw2IqMZV9vzELFYurVGkFamlbEG/OOEUDtSCnvt4LKl0HAWWotgNkKYl6Iw6o8/moMFXxmLB25f17ac7mOkHjDDHwqOGNVUciX5B+uZcYSyiM8xV19bsRxsBwJLZ9VkrlWAs7r4DfBVUrqlA9aPWrysjjRY+I3OfmSXGoCc/mOxMx8I5Ukjqfpvv73FmDlEOFBJc+q/VjWxFh5JbrsyeylbhvgA8hmInBlRcaE8QjqDXAPA2dOFm8JZRc6sojolapDYdmTgtjFJce5c2a0kPVMOm6YE8QScNJTrYjDwBi+CkW+CrdWCARuDZVZZl19m7xfIS1Qk27qfyvnarND0cDu2bvIT6b+9qsKDuwZXG5DT662egFi4Kfd6jF15GK96vb+7It/U9+fz9680Ec64mD2E6XXxoPh/jZx6N0w5Ggi+64bLl+264GleOJg713ngOjXphEIphHRTp5kkas+yFGxkRSb19On81f8MgXBXJCJ9OaC0Kvxd8eC0x52WziIZxMXnzH/X/yR9/AeOz/fivBQAA", 72 | "encoding": "UTF-8", 73 | "string": "" 74 | }, 75 | "headers": { 76 | "CF-RAY": "27559f82918339ee-PHX", 77 | "Connection": "keep-alive", 78 | "Content-Encoding": "gzip", 79 | "Content-Length": "615", 80 | "Content-Type": "application/json; charset=UTF-8", 81 | "Date": "Tue, 16 Feb 2016 01:54:11 GMT", 82 | "Server": "cloudflare-nginx", 83 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 84 | "Vary": "accept-encoding", 85 | "X-Moose": "majestic", 86 | "access-control-allow-origin": "*", 87 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 88 | "cache-control": "max-age=0, must-revalidate", 89 | "x-content-type-options": "nosniff", 90 | "x-frame-options": "SAMEORIGIN", 91 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=bgtPvC%2Bw01QbvHgBqBe9KjPxBnUDEhGJGA2xFUKvzSxUovACC4N3BUeLhOcTqrJIahzbUh8aADUeqGehAl871g2CGQtcV0Wv", 92 | "x-ua-compatible": "IE=edge", 93 | "x-xss-protection": "1; mode=block" 94 | }, 95 | "status": { 96 | "code": 200, 97 | "message": "OK" 98 | }, 99 | "url": "https://oauth.reddit.com/r/reddit_api_test/comments/45xjdr/want_raw_json_test/?raw_json=1" 100 | } 101 | } 102 | ], 103 | "recorded_with": "betamax/0.5.1" 104 | } 105 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__redirect.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-03-13T00:19:46", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "29", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=d696d0c07d476cd15a974d5a0b6cf29741455587650; loid=ol4hwS2uWoHYPNaM8g", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.4" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUNLNNS4xyiwK9Igyt/DNqiosdvJO9MzIyfc1KHdV0lFQAiuML6ksSAWpTkpNLEotAomnVhRkFqUWx2eCTDE2MzDQUVAqTs6HKNNSqgUA8qq4R2oAAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "282b50f3a71639d0-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sun, 13 Mar 2016 00:19:45 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-03-13T00:19:46", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer -m3t2irQHZ78MjzqsBKaIhloM0wE", 62 | "Connection": "keep-alive", 63 | "Cookie": "__cfduid=d696d0c07d476cd15a974d5a0b6cf29741455587650; loid=ol4hwS2uWoHYPNaM8g", 64 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.4" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/r/random?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "encoding": "UTF-8", 72 | "string": "\n \n 302 Found\n \n \n

302 Found

\n The resource was found at https://oauth.reddit.com/r/RelayForReddit/.json?raw_json=1;\nyou should be redirected automatically.\n\n\n \n" 73 | }, 74 | "headers": { 75 | "CF-RAY": "282b50f4f2b23988-PHX", 76 | "Connection": "keep-alive", 77 | "Content-Length": "299", 78 | "Content-Type": "text/html; charset=UTF-8", 79 | "Date": "Sun, 13 Mar 2016 00:19:46 GMT", 80 | "Server": "cloudflare-nginx", 81 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 82 | "X-Content-Type-Options": "nosniff", 83 | "X-Moose": "majestic", 84 | "cache-control": "max-age=0, must-revalidate", 85 | "location": "https://oauth.reddit.com/r/RelayForReddit/.json?raw_json=1", 86 | "x-ua-compatible": "IE=edge" 87 | }, 88 | "status": { 89 | "code": 302, 90 | "message": "Found" 91 | }, 92 | "url": "https://oauth.reddit.com/r/random?raw_json=1" 93 | } 94 | } 95 | ], 96 | "recorded_with": "betamax/0.5.1" 97 | } 98 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__redirect_301.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-30T00:09:45", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "29" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=p22PHVNAjmhSSmzLXS; loid=eRSV4OpMHfrI9sSU2d; redesign_optout=true; session_tracker=YaCPCgCQQP8Q53XnHF.0.1588120953197.Z0FBQUFBQmVxTTE1Tzd0eUxURl9ZZnlDbHAxedVJX19CNXhMOEVGaWh6cWthMUlGZDFMamVfZWdOYlVnemljTEs0cWttMFVxTXR4bUYyMzhXai0tY0RYckd4OGwINEF6dHpDabtqZDVXckd49TdxU1VrUWx1T1VOdzEtSldwWDU2oW5odlBiM1VYTkc" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.2.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/access_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "{\"access_token\": \"-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "109" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Wed, 30 Jun 2021 00:09:45 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "299" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "15" 89 | ], 90 | "x-ratelimit-used": [ 91 | "1" 92 | ], 93 | "x-reddit-loid": [ 94 | "0000000000d0lnaiza.2.1625011785709.Z0FBQUFBQmcyN1pKNTJ2SHN0STZIQnlTb3JQTGR0RXRBYmpTTDZOdUw1SE9QYlNZRWNkcHVpQUcyS2dJbFhsYnp4b1wkRzVKR0gxQ25BSDhMNkpBZkp6XzF4MUM3M0JsMlNNV0F690MxRVFWQ3rRLXBkNkpwVkpTOFpJY2dZMWJtSlVlVkVWV1FYaUE" 95 | ], 96 | "x-xss-protection": [ 97 | "1; mode=block" 98 | ] 99 | }, 100 | "status": { 101 | "code": 200, 102 | "message": "OK" 103 | }, 104 | "url": "https://www.reddit.com/api/v1/access_token" 105 | } 106 | }, 107 | { 108 | "recorded_at": "2021-06-30T00:09:45", 109 | "request": { 110 | "body": { 111 | "encoding": "utf-8", 112 | "string": "" 113 | }, 114 | "headers": { 115 | "Accept": [ 116 | "*/*" 117 | ], 118 | "Accept-Encoding": [ 119 | "gzip, deflate" 120 | ], 121 | "Authorization": [ 122 | "bearer -000000000000000000000000000000" 123 | ], 124 | "Connection": [ 125 | "keep-alive" 126 | ], 127 | "Cookie": [ 128 | "edgebucket=p22PHVNAjmhSSmzLXS; loid=eRSV4OpMHfrI9sSU2d; redesign_optout=true; session_tracker=YaCPCgCQQP8Q53XnHF.0.1588120953197.Z0FBQUFBQmVxTTE1Tzd0eUxURl9ZZnlDbHAxedVJX19CNXhMOEVGaWh6cWthMUlGZDFMamVfZWdOYlVnemljTEs0cWttMFVxTXR4bUYyMzhXai0tY0RYckd4OGwINEF6dHpDabtqZDVXckd49TdxU1VrUWx1T1VOdzEtSldwWDU2oW5odlBiM1VYTkc" 129 | ], 130 | "User-Agent": [ 131 | "prawcore:test (by /u/bboe) prawcore/2.2.0" 132 | ] 133 | }, 134 | "method": "GET", 135 | "uri": "https://oauth.reddit.com/t/bird?raw_json=1" 136 | }, 137 | "response": { 138 | "body": { 139 | "encoding": "utf-8", 140 | "string": "" 141 | }, 142 | "headers": { 143 | "Accept-Ranges": [ 144 | "bytes" 145 | ], 146 | "Connection": [ 147 | "keep-alive" 148 | ], 149 | "Content-Length": [ 150 | "0" 151 | ], 152 | "Content-Type": [ 153 | "text/html; charset=utf-8" 154 | ], 155 | "Date": [ 156 | "Wed, 30 Jun 2021 00:09:45 GMT" 157 | ], 158 | "Server": [ 159 | "snooserv" 160 | ], 161 | "Set-Cookie": [ 162 | "loid=eRSV4OpMHfrI9sSU2d; Max-Age=63072000; Path=/; Domain=.reddit.com; SameSite=None; Secure", 163 | "csv=1; Max-Age=63072000; Domain=.reddit.com; Path=/; Secure; SameSite=None" 164 | ], 165 | "Strict-Transport-Security": [ 166 | "max-age=15552000; includeSubDomains; preload" 167 | ], 168 | "Via": [ 169 | "1.1 varnish" 170 | ], 171 | "X-Clacks-Overhead": [ 172 | "GNU Terry Pratchett" 173 | ], 174 | "X-Moose": [ 175 | "majestic" 176 | ], 177 | "cache-control": [ 178 | "max-age=0, must-revalidate" 179 | ], 180 | "location": [ 181 | "https://oauth.reddit.com/r/t:bird/" 182 | ] 183 | }, 184 | "status": { 185 | "code": 301, 186 | "message": "Moved Permanently" 187 | }, 188 | "url": "https://oauth.reddit.com/t/bird?raw_json=1" 189 | } 190 | } 191 | ], 192 | "recorded_with": "betamax/0.8.1" 193 | } 194 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__too__many_requests__without_retry_headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-05-25T01:26:39", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "29" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "User-Agent": [ 30 | "python-requests/2.25.1 prawcore/2.0.0" 31 | ] 32 | }, 33 | "method": "POST", 34 | "uri": "https://www.reddit.com/api/v1/access_token" 35 | }, 36 | "response": { 37 | "body": { 38 | "encoding": "UTF-8", 39 | "string": "{\"message\": \"Too Many Requests\", \"error\": 429}" 40 | }, 41 | "headers": { 42 | "Accept-Ranges": [ 43 | "bytes" 44 | ], 45 | "Connection": [ 46 | "close" 47 | ], 48 | "Content-Length": [ 49 | "46" 50 | ], 51 | "Content-Type": [ 52 | "application/json; charset=UTF-8" 53 | ], 54 | "Date": [ 55 | "Tue, 25 May 2021 01:26:41 GMT" 56 | ], 57 | "Server": [ 58 | "snooserv" 59 | ], 60 | "Strict-Transport-Security": [ 61 | "max-age=15552000; includeSubDomains; preload" 62 | ], 63 | "Via": [ 64 | "1.1 varnish" 65 | ], 66 | "X-Moose": [ 67 | "majestic" 68 | ], 69 | "cache-control": [ 70 | "max-age=0, must-revalidate" 71 | ], 72 | "set-cookie": [ 73 | "loid=0000000000cbfksba7.2.1621906001718.Z0FBQUFBQmdyRkpSbC1uQTdNNDhncTI4cDRyTk1sb0xlUEJfZzhjVkVCdjI1WWxySnNBaG5tcVBfb1c5ZlI5aUJvWk5jSm1uQlVXYjQyY1Nna3k3NUN5b1hHalJsQ3FiVlZKWHUtbXRZQWhTWXk0bDufWGloZzdyY1RPaElkaE12dTZNN1RsblFSUHQ; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Thu, 25-May-2023 01:26:41 GMT; secure", 74 | "session_tracker=txIKGQAv1AKgdUH1IA.0.1621906001718.Z0FBQUFBQmdyRkpSbjVMdlJYYU0zYXRuYjA2dERQUkxVbU5yWnF2b3R3ZDVmbTdmd1prejhfcFRaUHvTQTRjbwFoeGtVQ01oZkpBeXVNUlJNelVmZWVETUlCel83cGxfckRieXVwUFpqRVBtdDl6d01qSGFZTy1FMHhuaGszbzh4YTlMTUQ5kVZqZE4; Domain=reddit.com; Max-Age=7199; Path=/; expires=Tue, 25-May-2021 03:26:41 GMT; secure", 75 | "edgebucket=3pCscH0HWW3O5QUtVI; Domain=reddit.com; Max-Age=63071999; Path=/; secure" 76 | ], 77 | "x-content-type-options": [ 78 | "nosniff" 79 | ], 80 | "x-frame-options": [ 81 | "SAMEORIGIN" 82 | ], 83 | "x-ua-compatible": [ 84 | "IE=edge" 85 | ], 86 | "x-xss-protection": [ 87 | "1; mode=block" 88 | ] 89 | }, 90 | "status": { 91 | "code": 429, 92 | "message": "Too Many Requests" 93 | }, 94 | "url": "https://www.reddit.com/api/v1/access_token" 95 | } 96 | } 97 | ], 98 | "recorded_with": "betamax/0.8.1" 99 | } 100 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__unavailable_for_legal_reasons.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-25T05:56:29", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "29", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.14" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUArPii/KdA33rYpyLgwqD0zLTKsscE8sccr2M1DSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQCx6bshaQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2c7d5e77e84c2246-LAX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Mon, 25 Jul 2016 05:56:30 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-25T05:56:31", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer Wj_riEWMzZCqRwQfifypGatBkN0", 62 | "Connection": "keep-alive", 63 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 64 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.14" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "encoding": "UTF-8", 72 | "string": "" 73 | }, 74 | "headers": { 75 | "CF-RAY": "2c7d5e791e530d55-LAX", 76 | "Connection": "keep-alive", 77 | "Content-Length": "0", 78 | "Content-Type": "application/json; charset=UTF-8", 79 | "Date": "Mon, 25 Jul 2016 05:56:30 GMT", 80 | "Server": "cloudflare-nginx", 81 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 82 | "Vary": "accept-encoding", 83 | "X-Moose": "majestic", 84 | "access-control-allow-origin": "*", 85 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 86 | "cache-control": "max-age=0, must-revalidate", 87 | "x-content-type-options": "nosniff", 88 | "x-frame-options": "SAMEORIGIN", 89 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=GY%2Bv5o1NEOkxMgWCHm6zCKj%2B7u80ZCqTWt65iM0v5Wfn1vYUKxs3laAkW9odHmaekQf8cCT5fGFa9Ohu7MqeYOGH%2BHmOdpBn", 90 | "x-ua-compatible": "IE=edge", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 451, 95 | "message": "UNAVAILABLE FOR LEGAL REASONS" 96 | }, 97 | "url": "https://oauth.reddit.com/?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.7.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__unexpected_status_code.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-07-09T23:02:18", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=password&password=&username=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "57", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 18 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUMopjPSKNDVLqkpPyfVwsihKNzbJ8DZzLTZJS1fSUVACq4svqSxIBSlOSk0sSi0CiadWFGQWpRbHZ4IMMTYzMNBRUCpOzoco01KqBQCnwDS+aQAAAA==", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2bff671aee2f2969-DUB", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Sat, 09 Jul 2016 23:02:18 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-07-09T23:02:18", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer lqYJY56bzgdmHB8rg34hK6Es4fg", 62 | "Connection": "keep-alive", 63 | "Content-Length": "0", 64 | "Cookie": "loid=S1UX7gEWPLHZFXDMKZ; __cfduid=dd96ea2981f3ad6ab5250a89b33e0ace81458613117", 65 | "User-Agent": "prawcore:test (by /u/bboe) prawcore/0.0.9" 66 | }, 67 | "method": "DELETE", 68 | "uri": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 69 | }, 70 | "response": { 71 | "body": { 72 | "encoding": "UTF-8", 73 | "string": "" 74 | }, 75 | "headers": { 76 | "CF-RAY": "2bff671f9154296f-DUB", 77 | "Connection": "keep-alive", 78 | "Content-Length": "0", 79 | "Content-Type": "application/json; charset=UTF-8", 80 | "Date": "Sat, 09 Jul 2016 23:02:18 GMT", 81 | "Server": "cloudflare-nginx", 82 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 83 | "X-Moose": "majestic", 84 | "cache-control": "private, s-maxage=0, max-age=0, must-revalidate", 85 | "expires": "-1", 86 | "x-content-type-options": "nosniff", 87 | "x-frame-options": "SAMEORIGIN", 88 | "x-ratelimit-remaining": "598.0", 89 | "x-ratelimit-reset": "462", 90 | "x-ratelimit-used": "2", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 205, 95 | "message": "Reset Content" 96 | }, 97 | "url": "https://oauth.reddit.com/api/v1/me/friends/spez?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.7.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__with_insufficient_scope.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-16T01:44:30", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=refresh_token&refresh_token=" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "74", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDIzMjWwMLbQdS9MLfN0Co3MDnYP946w1DUwyA3ILTa1CMuIVNJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhygrSk1MUaoFAOoj7j10AAAA", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "2755915526c33994-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Tue, 16 Feb 2016 01:44:30 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-02-16T01:44:30", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer 6250838-GqevIBUYkSGWKX9-00mPms58VhY", 62 | "Connection": "keep-alive", 63 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 64 | "User-Agent": "prawcore/0.0.1a1" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/api/v1/me?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "encoding": "UTF-8", 72 | "string": "{\"error\": 403}" 73 | }, 74 | "headers": { 75 | "CF-RAY": "27559155e15139a0-PHX", 76 | "Connection": "keep-alive", 77 | "Content-Length": "14", 78 | "Content-Type": "application/json; charset=UTF-8", 79 | "Date": "Tue, 16 Feb 2016 01:44:30 GMT", 80 | "Server": "cloudflare-nginx", 81 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 82 | "X-Moose": "majestic", 83 | "access-control-allow-origin": "*", 84 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 85 | "cache-control": "max-age=0, must-revalidate", 86 | "www-authenticate": "Bearer realm=\"reddit\", error=\"insufficient_scope\"", 87 | "x-content-type-options": "nosniff", 88 | "x-frame-options": "SAMEORIGIN", 89 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=2m2gh5E260EbUJ8L8zNd5P6oBA8uEn%2FOFMSg0pcZ5WeqCc6wvNMRoteGl4W45LJuZ8h2CahhLnV2FWOBjXpBuRhfhvo6cDQk", 90 | "x-ua-compatible": "IE=edge", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 403, 95 | "message": "Forbidden" 96 | }, 97 | "url": "https://oauth.reddit.com/api/v1/me?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.5.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestSession.test_request__with_invalid_access_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2016-02-16T01:20:45", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "grant_type=client_credentials" 9 | }, 10 | "headers": { 11 | "Accept": "*/*", 12 | "Accept-Encoding": "gzip, deflate", 13 | "Authorization": "Basic ", 14 | "Connection": "keep-alive", 15 | "Content-Length": "29", 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 18 | "User-Agent": "prawcore/0.0.1a1" 19 | }, 20 | "method": "POST", 21 | "uri": "https://www.reddit.com/api/v1/access_token" 22 | }, 23 | "response": { 24 | "body": { 25 | "base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUNJ1Ts3T9TP08QxKqsoqLnRytCjP8k3Ps8gz9chX0lFQAiuML6ksSAWpTkpNLEotAomnVhRkFqUWx2eCTDE2MzDQUVAqTs6HKNNSqgUAvqp/b2oAAAA=", 26 | "encoding": "UTF-8", 27 | "string": "" 28 | }, 29 | "headers": { 30 | "CF-RAY": "27556e8d569b3988-PHX", 31 | "Connection": "keep-alive", 32 | "Content-Encoding": "gzip", 33 | "Content-Type": "application/json; charset=UTF-8", 34 | "Date": "Tue, 16 Feb 2016 01:20:45 GMT", 35 | "Server": "cloudflare-nginx", 36 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 37 | "Transfer-Encoding": "chunked", 38 | "X-Moose": "majestic", 39 | "cache-control": "max-age=0, must-revalidate", 40 | "x-content-type-options": "nosniff", 41 | "x-frame-options": "SAMEORIGIN", 42 | "x-xss-protection": "1; mode=block" 43 | }, 44 | "status": { 45 | "code": 200, 46 | "message": "OK" 47 | }, 48 | "url": "https://www.reddit.com/api/v1/access_token" 49 | } 50 | }, 51 | { 52 | "recorded_at": "2016-02-16T01:20:46", 53 | "request": { 54 | "body": { 55 | "encoding": "utf-8", 56 | "string": "" 57 | }, 58 | "headers": { 59 | "Accept": "*/*", 60 | "Accept-Encoding": "gzip, deflate", 61 | "Authorization": "bearer -Cen-N1LIRbzjsqBA8wjMgn8n5Hoinvalid", 62 | "Connection": "keep-alive", 63 | "Cookie": "__cfduid=dd555eb868b1bcfd517d08fcb174c3afc1454806972", 64 | "User-Agent": "prawcore/0.0.1a1" 65 | }, 66 | "method": "GET", 67 | "uri": "https://oauth.reddit.com/?raw_json=1" 68 | }, 69 | "response": { 70 | "body": { 71 | "encoding": "UTF-8", 72 | "string": "{\"error\": 401}" 73 | }, 74 | "headers": { 75 | "CF-RAY": "27556e8e03533982-PHX", 76 | "Connection": "keep-alive", 77 | "Content-Length": "14", 78 | "Content-Type": "application/json; charset=UTF-8", 79 | "Date": "Tue, 16 Feb 2016 01:20:46 GMT", 80 | "Server": "cloudflare-nginx", 81 | "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", 82 | "X-Moose": "majestic", 83 | "access-control-allow-origin": "*", 84 | "access-control-expose-headers": "X-Reddit-Tracking, X-Moose", 85 | "cache-control": "max-age=0, must-revalidate", 86 | "www-authenticate": "Bearer realm=\"reddit\", error=\"invalid_token\"", 87 | "x-content-type-options": "nosniff", 88 | "x-frame-options": "SAMEORIGIN", 89 | "x-reddit-tracking": "https://pixel.redditmedia.com/pixel/of_destiny.png?v=aEP1d6ec63%2Bkh0UX%2BuLcmukE4MSF2GGFmt1Hae2P0RrrwuQw%2BpxyRVNWJXPHvpGeGTjlZFcKJGL3P5laYED2QqNXIwtdtonG", 90 | "x-ua-compatible": "IE=edge", 91 | "x-xss-protection": "1; mode=block" 92 | }, 93 | "status": { 94 | "code": 401, 95 | "message": "Unauthorized" 96 | }, 97 | "url": "https://oauth.reddit.com/?raw_json=1" 98 | } 99 | } 100 | ], 101 | "recorded_with": "betamax/0.5.1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestTrustedAuthenticator.test_revoke_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-07T09:30:24", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "token=dummy+token" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "17" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "User-Agent": [ 30 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 31 | ] 32 | }, 33 | "method": "POST", 34 | "uri": "https://www.reddit.com/api/v1/revoke_token" 35 | }, 36 | "response": { 37 | "body": { 38 | "encoding": "UTF-8", 39 | "string": "" 40 | }, 41 | "headers": { 42 | "Accept-Ranges": [ 43 | "bytes" 44 | ], 45 | "Connection": [ 46 | "close" 47 | ], 48 | "Content-Length": [ 49 | "0" 50 | ], 51 | "Content-Type": [ 52 | "application/json; charset=UTF-8" 53 | ], 54 | "Date": [ 55 | "Mon, 07 Jun 2021 09:30:24 GMT" 56 | ], 57 | "Server": [ 58 | "snooserv" 59 | ], 60 | "Set-Cookie": [ 61 | "edgebucket=hhWRRZiFFiiGuzWIo9; Domain=reddit.com; Max-Age=63071999; Path=/; secure" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "299" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "576" 89 | ], 90 | "x-ratelimit-used": [ 91 | "1" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/revoke_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestTrustedAuthenticator.test_revoke_token__with_access_token_hint.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-07T09:30:24", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "token=dummy+token&token_type_hint=access_token" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "46" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=S3wlJDCxzOhDOBJEN2" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/revoke_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "0" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Mon, 07 Jun 2021 09:30:25 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "298" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "575" 89 | ], 90 | "x-ratelimit-used": [ 91 | "2" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/revoke_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestTrustedAuthenticator.test_revoke_token__with_refresh_token_hint.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-07T09:30:24", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "token=dummy+token&token_type_hint=refresh_token" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "47" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "Cookie": [ 30 | "edgebucket=k0a87qutnOD8PfGB2w" 31 | ], 32 | "User-Agent": [ 33 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 34 | ] 35 | }, 36 | "method": "POST", 37 | "uri": "https://www.reddit.com/api/v1/revoke_token" 38 | }, 39 | "response": { 40 | "body": { 41 | "encoding": "UTF-8", 42 | "string": "" 43 | }, 44 | "headers": { 45 | "Accept-Ranges": [ 46 | "bytes" 47 | ], 48 | "Connection": [ 49 | "close" 50 | ], 51 | "Content-Length": [ 52 | "0" 53 | ], 54 | "Content-Type": [ 55 | "application/json; charset=UTF-8" 56 | ], 57 | "Date": [ 58 | "Mon, 07 Jun 2021 09:30:25 GMT" 59 | ], 60 | "Server": [ 61 | "snooserv" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "297" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "575" 89 | ], 90 | "x-ratelimit-used": [ 91 | "3" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/revoke_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/cassettes/TestUntrustedAuthenticator.test_revoke_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2021-06-07T09:26:10", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "token=dummy+token" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Authorization": [ 18 | "Basic " 19 | ], 20 | "Connection": [ 21 | "close" 22 | ], 23 | "Content-Length": [ 24 | "17" 25 | ], 26 | "Content-Type": [ 27 | "application/x-www-form-urlencoded" 28 | ], 29 | "User-Agent": [ 30 | "prawcore:test (by /u/bboe) prawcore/2.0.0" 31 | ] 32 | }, 33 | "method": "POST", 34 | "uri": "https://www.reddit.com/api/v1/revoke_token" 35 | }, 36 | "response": { 37 | "body": { 38 | "encoding": "UTF-8", 39 | "string": "" 40 | }, 41 | "headers": { 42 | "Accept-Ranges": [ 43 | "bytes" 44 | ], 45 | "Connection": [ 46 | "close" 47 | ], 48 | "Content-Length": [ 49 | "0" 50 | ], 51 | "Content-Type": [ 52 | "application/json; charset=UTF-8" 53 | ], 54 | "Date": [ 55 | "Mon, 07 Jun 2021 09:26:10 GMT" 56 | ], 57 | "Server": [ 58 | "snooserv" 59 | ], 60 | "Set-Cookie": [ 61 | "edgebucket=0myJx5ttzvTVXbZNwZ; Domain=reddit.com; Max-Age=63071999; Path=/; secure" 62 | ], 63 | "Strict-Transport-Security": [ 64 | "max-age=15552000; includeSubDomains; preload" 65 | ], 66 | "Via": [ 67 | "1.1 varnish" 68 | ], 69 | "X-Clacks-Overhead": [ 70 | "GNU Terry Pratchett" 71 | ], 72 | "X-Moose": [ 73 | "majestic" 74 | ], 75 | "cache-control": [ 76 | "max-age=0, must-revalidate" 77 | ], 78 | "x-content-type-options": [ 79 | "nosniff" 80 | ], 81 | "x-frame-options": [ 82 | "SAMEORIGIN" 83 | ], 84 | "x-ratelimit-remaining": [ 85 | "299" 86 | ], 87 | "x-ratelimit-reset": [ 88 | "230" 89 | ], 90 | "x-ratelimit-used": [ 91 | "1" 92 | ], 93 | "x-xss-protection": [ 94 | "1; mode=block" 95 | ] 96 | }, 97 | "status": { 98 | "code": 200, 99 | "message": "OK" 100 | }, 101 | "url": "https://www.reddit.com/api/v1/revoke_token" 102 | } 103 | } 104 | ], 105 | "recorded_with": "betamax/0.8.1" 106 | } 107 | -------------------------------------------------------------------------------- /tests/integration/files/too_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praw-dev/prawcore/165e18e82ba76129777a2d9e4b7cb0065066bb32/tests/integration/files/too_large.jpg -------------------------------------------------------------------------------- /tests/integration/files/white-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praw-dev/prawcore/165e18e82ba76129777a2d9e4b7cb0065066bb32/tests/integration/files/white-square.png -------------------------------------------------------------------------------- /tests/integration/test_authenticator.py: -------------------------------------------------------------------------------- 1 | """Test for subclasses of prawcore.auth.BaseAuthenticator class.""" 2 | 3 | import pytest 4 | 5 | import prawcore 6 | 7 | from . import IntegrationTest 8 | 9 | 10 | class TestTrustedAuthenticator(IntegrationTest): 11 | def test_revoke_token(self, requestor): 12 | authenticator = prawcore.TrustedAuthenticator( 13 | requestor, 14 | pytest.placeholders.client_id, 15 | pytest.placeholders.client_secret, 16 | ) 17 | authenticator.revoke_token("dummy token") 18 | 19 | def test_revoke_token__with_access_token_hint(self, requestor): 20 | authenticator = prawcore.TrustedAuthenticator( 21 | requestor, 22 | pytest.placeholders.client_id, 23 | pytest.placeholders.client_secret, 24 | ) 25 | authenticator.revoke_token("dummy token", "access_token") 26 | 27 | def test_revoke_token__with_refresh_token_hint(self, requestor): 28 | authenticator = prawcore.TrustedAuthenticator( 29 | requestor, 30 | pytest.placeholders.client_id, 31 | pytest.placeholders.client_secret, 32 | ) 33 | authenticator.revoke_token("dummy token", "refresh_token") 34 | 35 | 36 | class TestUntrustedAuthenticator(IntegrationTest): 37 | def test_revoke_token(self, requestor): 38 | authenticator = prawcore.UntrustedAuthenticator(requestor, pytest.placeholders.client_id) 39 | authenticator.revoke_token("dummy token") 40 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """prawcore Unit test suite.""" 2 | 3 | 4 | class UnitTest: 5 | """Base class for prawcore unit tests.""" 6 | -------------------------------------------------------------------------------- /tests/unit/test_authenticator.py: -------------------------------------------------------------------------------- 1 | """Test for subclasses of prawcore.auth.BaseAuthenticator class.""" 2 | 3 | import pytest 4 | 5 | import prawcore 6 | 7 | from . import UnitTest 8 | 9 | 10 | class TestTrustedAuthenticator(UnitTest): 11 | @pytest.fixture 12 | def trusted_authenticator(self, trusted_authenticator): 13 | trusted_authenticator.redirect_uri = pytest.placeholders.redirect_uri 14 | return trusted_authenticator 15 | 16 | def test_authorize_url(self, trusted_authenticator): 17 | url = trusted_authenticator.authorize_url("permanent", ["identity", "read"], "a_state") 18 | assert f"client_id={pytest.placeholders.client_id}" in url 19 | assert "duration=permanent" in url 20 | assert "response_type=code" in url 21 | assert "scope=identity+read" in url 22 | assert "state=a_state" in url 23 | 24 | def test_authorize_url__fail_with_implicit(self, trusted_authenticator): 25 | with pytest.raises(prawcore.InvalidInvocation): 26 | trusted_authenticator.authorize_url("temporary", ["identity", "read"], "a_state", implicit=True) 27 | 28 | def test_authorize_url__fail_without_redirect_uri(self, trusted_authenticator): 29 | trusted_authenticator.redirect_uri = None 30 | with pytest.raises(prawcore.InvalidInvocation): 31 | trusted_authenticator.authorize_url( 32 | "permanent", 33 | ["identity"], 34 | "...", 35 | ) 36 | 37 | 38 | class TestUntrustedAuthenticator(UnitTest): 39 | @pytest.fixture 40 | def untrusted_authenticator(self, untrusted_authenticator): 41 | untrusted_authenticator.redirect_uri = pytest.placeholders.redirect_uri 42 | return untrusted_authenticator 43 | 44 | def test_authorize_url__code(self, untrusted_authenticator): 45 | url = untrusted_authenticator.authorize_url("permanent", ["identity", "read"], "a_state") 46 | assert f"client_id={pytest.placeholders.client_id}" in url 47 | assert "duration=permanent" in url 48 | assert "response_type=code" in url 49 | assert "scope=identity+read" in url 50 | assert "state=a_state" in url 51 | 52 | def test_authorize_url__fail_with_token_and_permanent(self, untrusted_authenticator): 53 | with pytest.raises(prawcore.InvalidInvocation): 54 | untrusted_authenticator.authorize_url( 55 | "permanent", 56 | ["identity", "read"], 57 | "a_state", 58 | implicit=True, 59 | ) 60 | 61 | def test_authorize_url__fail_without_redirect_uri(self, untrusted_authenticator): 62 | untrusted_authenticator.redirect_uri = None 63 | with pytest.raises(prawcore.InvalidInvocation): 64 | untrusted_authenticator.authorize_url( 65 | "temporary", 66 | ["identity"], 67 | "...", 68 | ) 69 | 70 | def test_authorize_url__token(self, untrusted_authenticator): 71 | url = untrusted_authenticator.authorize_url("temporary", ["identity", "read"], "a_state", implicit=True) 72 | assert f"client_id={pytest.placeholders.client_id}" in url 73 | assert "duration=temporary" in url 74 | assert "response_type=token" in url 75 | assert "scope=identity+read" in url 76 | assert "state=a_state" in url 77 | -------------------------------------------------------------------------------- /tests/unit/test_authorizer.py: -------------------------------------------------------------------------------- 1 | """Test for prawcore.auth.Authorizer classes.""" 2 | 3 | import pytest 4 | 5 | import prawcore 6 | 7 | from . import UnitTest 8 | 9 | 10 | class InvalidAuthenticator(prawcore.auth.BaseAuthenticator): 11 | _auth = None 12 | 13 | 14 | class TestAuthorizer(UnitTest): 15 | def test_authorize__fail_without_redirect_uri(self, trusted_authenticator): 16 | authorizer = prawcore.Authorizer(trusted_authenticator) 17 | with pytest.raises(prawcore.InvalidInvocation): 18 | authorizer.authorize("dummy code") 19 | assert not authorizer.is_valid() 20 | 21 | def test_initialize(self, trusted_authenticator): 22 | authorizer = prawcore.Authorizer(trusted_authenticator) 23 | assert authorizer.access_token is None 24 | assert authorizer.scopes is None 25 | assert authorizer.refresh_token is None 26 | assert not authorizer.is_valid() 27 | 28 | def test_initialize__with_refresh_token(self, trusted_authenticator): 29 | authorizer = prawcore.Authorizer(trusted_authenticator, refresh_token=pytest.placeholders.refresh_token) 30 | assert authorizer.access_token is None 31 | assert authorizer.scopes is None 32 | assert pytest.placeholders.refresh_token == authorizer.refresh_token 33 | assert not authorizer.is_valid() 34 | 35 | def test_initialize__with_untrusted_authenticator(self): 36 | authenticator = prawcore.UntrustedAuthenticator(None, None) 37 | authorizer = prawcore.Authorizer(authenticator) 38 | assert authorizer.access_token is None 39 | assert authorizer.scopes is None 40 | assert authorizer.refresh_token is None 41 | assert not authorizer.is_valid() 42 | 43 | def test_refresh__without_refresh_token(self, trusted_authenticator): 44 | authorizer = prawcore.Authorizer(trusted_authenticator) 45 | with pytest.raises(prawcore.InvalidInvocation): 46 | authorizer.refresh() 47 | assert not authorizer.is_valid() 48 | 49 | def test_revoke__without_access_token(self, trusted_authenticator): 50 | authorizer = prawcore.Authorizer(trusted_authenticator, refresh_token=pytest.placeholders.refresh_token) 51 | with pytest.raises(prawcore.InvalidInvocation): 52 | authorizer.revoke(only_access=True) 53 | 54 | def test_revoke__without_any_token(self, trusted_authenticator): 55 | authorizer = prawcore.Authorizer(trusted_authenticator) 56 | with pytest.raises(prawcore.InvalidInvocation): 57 | authorizer.revoke() 58 | 59 | 60 | class TestDeviceIDAuthorizer(UnitTest): 61 | def test_initialize(self, untrusted_authenticator): 62 | authorizer = prawcore.DeviceIDAuthorizer(untrusted_authenticator) 63 | assert authorizer.access_token is None 64 | assert authorizer.scopes is None 65 | assert not authorizer.is_valid() 66 | 67 | def test_initialize__with_invalid_authenticator(self): 68 | authenticator = prawcore.Authorizer(InvalidAuthenticator(None, None, None)) 69 | with pytest.raises(prawcore.InvalidInvocation): 70 | prawcore.DeviceIDAuthorizer(authenticator) 71 | 72 | 73 | class TestImplicitAuthorizer(UnitTest): 74 | def test_initialize(self, untrusted_authenticator): 75 | authorizer = prawcore.ImplicitAuthorizer(untrusted_authenticator, "fake token", 1, "modposts read") 76 | assert authorizer.access_token == "fake token" 77 | assert authorizer.scopes == {"modposts", "read"} 78 | assert authorizer.is_valid() 79 | 80 | def test_initialize__with_trusted_authenticator(self, trusted_authenticator): 81 | with pytest.raises(prawcore.InvalidInvocation): 82 | prawcore.ImplicitAuthorizer(trusted_authenticator, None, None, None) 83 | 84 | 85 | class TestReadOnlyAuthorizer(UnitTest): 86 | def test_initialize__with_untrusted_authenticator(self, untrusted_authenticator): 87 | with pytest.raises(prawcore.InvalidInvocation): 88 | prawcore.ReadOnlyAuthorizer(untrusted_authenticator) 89 | 90 | 91 | class TestScriptAuthorizer(UnitTest): 92 | def test_initialize__with_untrusted_authenticator(self, untrusted_authenticator): 93 | with pytest.raises(prawcore.InvalidInvocation): 94 | prawcore.ScriptAuthorizer(untrusted_authenticator, None, None) 95 | -------------------------------------------------------------------------------- /tests/unit/test_rate_limit.py: -------------------------------------------------------------------------------- 1 | """Test for prawcore.Sessions module.""" 2 | 3 | from copy import copy 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from prawcore.const import NANOSECONDS 9 | from prawcore.rate_limit import RateLimiter 10 | 11 | from . import UnitTest 12 | 13 | 14 | class TestRateLimiter(UnitTest): 15 | @pytest.fixture 16 | def rate_limiter(self): 17 | rate_limiter = RateLimiter(window_size=600) 18 | rate_limiter.next_request_timestamp_ns = 100 * NANOSECONDS 19 | return rate_limiter 20 | 21 | @staticmethod 22 | def _headers(remaining, used, reset): 23 | return { 24 | "x-ratelimit-remaining": str(float(remaining)), 25 | "x-ratelimit-used": str(used), 26 | "x-ratelimit-reset": str(reset), 27 | } 28 | 29 | @patch("time.monotonic_ns") 30 | @patch("time.sleep") 31 | def test_delay(self, mock_sleep, mock_monotonic_ns, rate_limiter): 32 | mock_monotonic_ns.return_value = 1 * NANOSECONDS 33 | rate_limiter.delay() 34 | assert mock_monotonic_ns.called 35 | mock_sleep.assert_called_with(99) 36 | 37 | @patch("time.monotonic_ns") 38 | @patch("time.sleep") 39 | def test_delay__no_sleep_when_time_in_past(self, mock_sleep, mock_monotonic_ns, rate_limiter): 40 | mock_monotonic_ns.return_value = 101 * NANOSECONDS 41 | rate_limiter.delay() 42 | assert mock_monotonic_ns.called 43 | assert not mock_sleep.called 44 | 45 | @patch("time.sleep") 46 | def test_delay__no_sleep_when_time_is_not_set(self, mock_sleep, rate_limiter): 47 | rate_limiter.next_request_timestamp_ns = None 48 | rate_limiter.delay() 49 | assert not mock_sleep.called 50 | 51 | @patch("time.monotonic_ns") 52 | @patch("time.sleep") 53 | def test_delay__no_sleep_when_times_match(self, mock_sleep, mock_monotonic_ns, rate_limiter): 54 | mock_monotonic_ns.return_value = 100 * NANOSECONDS 55 | rate_limiter.delay() 56 | assert mock_monotonic_ns.called 57 | assert not mock_sleep.called 58 | 59 | @patch("time.monotonic_ns") 60 | def test_update__compute_delay_with_no_previous_info(self, mock_monotonic_ns, rate_limiter): 61 | mock_monotonic_ns.return_value = 100 * NANOSECONDS 62 | rate_limiter.update(self._headers(60, 100, 60)) 63 | assert rate_limiter.remaining == 60 64 | assert rate_limiter.used == 100 65 | assert rate_limiter.next_request_timestamp_ns == 100 * NANOSECONDS 66 | 67 | @patch("time.monotonic_ns") 68 | def test_update__compute_delay_with_single_client(self, mock_monotonic_ns, rate_limiter): 69 | rate_limiter.window_size = 150 70 | mock_monotonic_ns.return_value = 100 * NANOSECONDS 71 | rate_limiter.update(self._headers(50, 100, 60)) 72 | assert rate_limiter.remaining == 50 73 | assert rate_limiter.used == 100 74 | assert rate_limiter.next_request_timestamp_ns == 110 * NANOSECONDS 75 | 76 | @patch("time.monotonic_ns") 77 | def test_update__compute_delay_with_six_clients(self, mock_monotonic_ns, rate_limiter): 78 | rate_limiter.remaining = 66 79 | rate_limiter.window_size = 180 80 | mock_monotonic_ns.return_value = 100 * NANOSECONDS 81 | rate_limiter.update(self._headers(60, 100, 72)) 82 | assert rate_limiter.remaining == 60 83 | assert rate_limiter.used == 100 84 | assert rate_limiter.next_request_timestamp_ns == 104.5 * NANOSECONDS 85 | 86 | @patch("time.monotonic_ns") 87 | def test_update__delay_full_time_with_negative_remaining(self, mock_monotonic_ns, rate_limiter): 88 | mock_monotonic_ns.return_value = 37 * NANOSECONDS 89 | rate_limiter.update(self._headers(0, 100, 13)) 90 | assert rate_limiter.remaining == 0 91 | assert rate_limiter.used == 100 92 | assert rate_limiter.next_request_timestamp_ns == 50 * NANOSECONDS 93 | 94 | @patch("time.monotonic_ns") 95 | def test_update__delay_full_time_with_zero_remaining(self, mock_monotonic_ns, rate_limiter): 96 | mock_monotonic_ns.return_value = 37 * NANOSECONDS 97 | rate_limiter.update(self._headers(0, 100, 13)) 98 | assert rate_limiter.remaining == 0 99 | assert rate_limiter.used == 100 100 | assert rate_limiter.next_request_timestamp_ns == 50 * NANOSECONDS 101 | 102 | @patch("time.monotonic_ns") 103 | def test_update__delay_full_time_with_zero_remaining_and_no_sleep_time(self, mock_monotonic_ns, rate_limiter): 104 | mock_monotonic_ns.return_value = 37 * NANOSECONDS 105 | rate_limiter.update(self._headers(0, 100, 0)) 106 | assert rate_limiter.remaining == 0 107 | assert rate_limiter.used == 100 108 | assert rate_limiter.next_request_timestamp_ns == 38 * NANOSECONDS 109 | 110 | def test_update__no_change_without_headers(self, rate_limiter): 111 | prev = copy(rate_limiter) 112 | rate_limiter.update({}) 113 | assert prev.remaining == rate_limiter.remaining 114 | assert prev.used == rate_limiter.used 115 | assert rate_limiter.next_request_timestamp_ns == prev.next_request_timestamp_ns 116 | 117 | def test_update__values_change_without_headers(self, rate_limiter): 118 | rate_limiter.remaining = 10 119 | rate_limiter.used = 99 120 | rate_limiter.update({}) 121 | assert rate_limiter.remaining == 9 122 | assert rate_limiter.used == 100 123 | -------------------------------------------------------------------------------- /tests/unit/test_requestor.py: -------------------------------------------------------------------------------- 1 | """Test for prawcore.self.requestor.Requestor class.""" 2 | 3 | import pickle 4 | from inspect import signature 5 | from unittest.mock import Mock, patch 6 | 7 | import pytest 8 | 9 | import prawcore 10 | from prawcore import RequestException 11 | 12 | from . import UnitTest 13 | 14 | 15 | class TestRequestor(UnitTest): 16 | def test_initialize(self, requestor): 17 | assert requestor._http.headers["User-Agent"] == f"prawcore:test (by /u/bboe) prawcore/{prawcore.__version__}" 18 | 19 | def test_initialize__failures(self): 20 | for agent in [None, "shorty"]: 21 | with pytest.raises(prawcore.InvalidInvocation): 22 | prawcore.Requestor(agent) 23 | 24 | def test_pickle(self, requestor): 25 | for protocol in range(pickle.HIGHEST_PROTOCOL + 1): 26 | pickle.loads(pickle.dumps(requestor, protocol=protocol)) 27 | 28 | def test_request__session_timeout_default(self, requestor): 29 | requestor_signature = signature(requestor._http.request) 30 | assert str(requestor_signature.parameters["timeout"]) == "timeout=None" 31 | 32 | def test_request__use_custom_session(self): 33 | override = "REQUEST OVERRIDDEN" 34 | custom_header = "CUSTOM SESSION HEADER" 35 | headers = {"session_header": custom_header} 36 | attrs = {"request.return_value": override, "headers": headers} 37 | session = Mock(**attrs) 38 | 39 | requestor = prawcore.Requestor("prawcore:test (by /u/bboe)", session=session) 40 | 41 | assert requestor._http.headers["User-Agent"] == f"prawcore:test (by /u/bboe) prawcore/{prawcore.__version__}" 42 | assert requestor._http.headers["session_header"] == custom_header 43 | 44 | assert requestor.request("https://reddit.com") == override 45 | 46 | @patch("requests.Session") 47 | def test_request__wrap_request_exceptions(self, mock_session): 48 | exception = Exception("prawcore wrap_request_exceptions") 49 | session_instance = mock_session.return_value 50 | session_instance.request.side_effect = exception 51 | requestor = prawcore.Requestor("prawcore:test (by /u/bboe)") 52 | with pytest.raises(prawcore.RequestException) as exception_info: 53 | requestor.request("get", "http://a.b", data="bar") 54 | assert isinstance(exception_info.value, RequestException) 55 | assert exception is exception_info.value.original_exception 56 | assert exception_info.value.request_args == ("get", "http://a.b") 57 | assert exception_info.value.request_kwargs == {"data": "bar"} 58 | -------------------------------------------------------------------------------- /tests/unit/test_sessions.py: -------------------------------------------------------------------------------- 1 | """Test for prawcore.Sessions module.""" 2 | 3 | import logging 4 | from unittest.mock import Mock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ChunkedEncodingError, ConnectionError, ReadTimeout 8 | 9 | import prawcore 10 | from prawcore.exceptions import RequestException 11 | from prawcore.sessions import FiniteRetryStrategy 12 | 13 | from . import UnitTest 14 | 15 | 16 | class InvalidAuthorizer(prawcore.Authorizer): 17 | def __init__(self, requestor): 18 | super().__init__( 19 | prawcore.TrustedAuthenticator( 20 | requestor, 21 | pytest.placeholders.client_id, 22 | pytest.placeholders.client_secret, 23 | ) 24 | ) 25 | 26 | def is_valid(self): 27 | return False 28 | 29 | 30 | class TestSession(UnitTest): 31 | @pytest.fixture 32 | def readonly_authorizer(self, trusted_authenticator): 33 | return prawcore.ReadOnlyAuthorizer(trusted_authenticator) 34 | 35 | def test_close(self, readonly_authorizer): 36 | prawcore.Session(readonly_authorizer).close() 37 | 38 | def test_context_manager(self, readonly_authorizer): 39 | with prawcore.Session(readonly_authorizer) as session: 40 | assert isinstance(session, prawcore.Session) 41 | 42 | def test_init__with_device_id_authorizer(self, untrusted_authenticator): 43 | authorizer = prawcore.DeviceIDAuthorizer(untrusted_authenticator) 44 | prawcore.Session(authorizer) 45 | 46 | def test_init__with_implicit_authorizer(self, untrusted_authenticator): 47 | authorizer = prawcore.ImplicitAuthorizer(untrusted_authenticator, None, 0, "") 48 | prawcore.Session(authorizer) 49 | 50 | def test_init__without_authenticator(self): 51 | with pytest.raises(prawcore.InvalidInvocation): 52 | prawcore.Session(None) 53 | 54 | @patch("requests.Session") 55 | @pytest.mark.parametrize( 56 | "exception", 57 | [ChunkedEncodingError(), ConnectionError(), ReadTimeout()], 58 | ids=["ChunkedEncodingError", "ConnectionError", "ReadTimeout"], 59 | ) 60 | def test_request__retry(self, mock_session, exception, caplog): 61 | caplog.set_level(logging.WARNING) 62 | session_instance = mock_session.return_value 63 | # Handle Auth 64 | response_dict = {"access_token": "", "expires_in": 99, "scope": ""} 65 | session_instance.request.return_value = Mock(headers={}, json=lambda: response_dict, status_code=200) 66 | requestor = prawcore.Requestor("prawcore:test (by /u/bboe)") 67 | authenticator = prawcore.TrustedAuthenticator( 68 | requestor, 69 | pytest.placeholders.client_id, 70 | pytest.placeholders.client_secret, 71 | ) 72 | authorizer = prawcore.ReadOnlyAuthorizer(authenticator) 73 | authorizer.refresh() 74 | session_instance.request.reset_mock() 75 | # Fail on subsequent request 76 | session_instance.request.side_effect = exception 77 | 78 | with pytest.raises(RequestException) as exception_info: 79 | prawcore.Session(authorizer).request("GET", "/") 80 | assert ( 81 | "prawcore", 82 | logging.WARNING, 83 | f"Retrying due to {exception.__class__.__name__}(): GET https://oauth.reddit.com/", 84 | ) in caplog.record_tuples 85 | assert isinstance(exception_info.value, RequestException) 86 | assert exception is exception_info.value.original_exception 87 | assert session_instance.request.call_count == 3 88 | 89 | def test_request__with_invalid_authorizer(self, requestor): 90 | session = prawcore.Session(InvalidAuthorizer(requestor)) 91 | with pytest.raises(prawcore.InvalidInvocation): 92 | session.request("get", "/") 93 | 94 | 95 | class TestSessionFunction(UnitTest): 96 | def test_session(self, requestor): 97 | assert isinstance(prawcore.session(InvalidAuthorizer(requestor)), prawcore.Session) 98 | 99 | 100 | class TestFiniteRetryStrategy(UnitTest): 101 | @patch("time.sleep") 102 | def test_strategy(self, mock_sleep): 103 | strategy = FiniteRetryStrategy() 104 | assert strategy.should_retry_on_failure() 105 | strategy.sleep() 106 | mock_sleep.assert_not_called() 107 | 108 | strategy = strategy.consume_available_retry() 109 | assert strategy.should_retry_on_failure() 110 | strategy.sleep() 111 | assert len(calls := mock_sleep.mock_calls) == 1 112 | assert 0 < calls[0].args[0] < 2 113 | mock_sleep.reset_mock() 114 | 115 | strategy = strategy.consume_available_retry() 116 | assert not strategy.should_retry_on_failure() 117 | strategy.sleep() 118 | assert len(calls := mock_sleep.mock_calls) == 1 119 | assert 2 < calls[0].args[0] < 4 120 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Pytest utils for integration tests.""" 2 | 3 | import json 4 | 5 | import betamax 6 | from betamax.serializers import JSONSerializer 7 | 8 | 9 | def ensure_integration_test(cassette): 10 | if cassette.is_recording(): # pragma: no cover 11 | is_integration_test = bool(cassette.interactions) 12 | action = "record" 13 | else: 14 | is_integration_test = any(interaction.used for interaction in cassette.interactions) 15 | action = "play back" 16 | message = f"Cassette did not {action} any requests. This test can be a unit test." 17 | assert is_integration_test, message 18 | 19 | 20 | def filter_access_token(interaction, current_cassette): # pragma: no cover 21 | """Add Betamax placeholder to filter access token.""" 22 | request_uri = interaction.data["request"]["uri"] 23 | response = interaction.data["response"] 24 | if "api/v1/access_token" not in request_uri or response["status"]["code"] != 200: 25 | return 26 | body = response["body"]["string"] 27 | for token_key in ["access", "refresh"]: 28 | try: 29 | token = json.loads(body)[f"{token_key}_token"] 30 | except (KeyError, TypeError, ValueError): 31 | continue 32 | current_cassette.placeholders.append( 33 | betamax.cassette.cassette.Placeholder(placeholder=f"<{token_key.upper()}_TOKEN>", replace=token) 34 | ) 35 | 36 | 37 | class PrettyJSONSerializer(JSONSerializer): # pragma: no cover 38 | name = "prettyjson" 39 | 40 | def serialize(self, cassette_data): 41 | return f"{json.dumps(cassette_data, sort_keys=True, indent=2)}\n" 42 | --------------------------------------------------------------------------------