diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index eb29ff27..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,67 +0,0 @@ -version: 2 -jobs: - python_2: - docker: - - image: circleci/python:2.7-jessie - steps: - - checkout - - run: - command: | - sudo chown -R circleci:circleci /usr/local/bin - sudo chown -R circleci:circleci /usr/local/lib/python2.7/site-packages - - restore_cache: - key: deps2-{{ .Branch }}-{{ checksum "setup.py" }} - - run: - command: | - pip install .[test] - pip install codecov - - save_cache: - key: deps2-{{ .Branch }}-{{ checksum "setup.py" }} - paths: - - ".venv" - - "/usr/local/bin" - - "/usr/local/lib/python2.7/site-packages" - - run: - name: Run tests - command: coverage run --include=auth0/v3/*.py --omit=auth0/v3/test/*.py -m unittest discover - - run: - when: on_success - command: bash <(curl -s https://codecov.io/bash) - python_3: - docker: - - image: circleci/python:3.6-jessie - steps: - - checkout - - run: - command: | - sudo chown -R circleci:circleci /usr/local/bin - sudo chown -R circleci:circleci /usr/local/lib/python3.6/site-packages - - restore_cache: - key: deps3-{{ .Branch }}-{{ checksum "setup.py" }} - - run: - command: | - pip install .[test] - pip install codecov - pip install -r docs/requirements.txt - - save_cache: - key: deps3-{{ .Branch }}-{{ checksum "setup.py" }} - paths: - - ".venv" - - "/usr/local/bin" - - "/usr/local/lib/python3.6/site-packages" - - run: - name: Run tests - command: coverage run --include=auth0/v3/*.py --omit=auth0/v3/test/*.py -m unittest discover - - run: - name: Build docs - command: cd docs/ && make html - - run: - when: on_success - command: bash <(curl -s https://codecov.io/bash) - -workflows: - version: 2 - build: - jobs: - - python_2 - - python_3 \ No newline at end of file diff --git a/.codecov.yml b/.codecov.yml index 067d9743..3a889e2e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -19,4 +19,4 @@ coverage: default: enabled: true if_no_uploads: error -comment: false \ No newline at end of file +comment: false diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..5610cc02 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E501 F401 +max-line-length = 88 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 60f116c0..7958e8bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdks-engineer +* @auth0/project-dx-sdks-engineer-codeowner diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml new file mode 100644 index 00000000..31e6efa5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -0,0 +1,67 @@ +name: ๐Ÿž Report a bug +description: Have you found a bug or issue? Create a bug report for this library +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have looked into the [Readme](https://github.com/auth0/auth0-python#readme) and [Examples](https://github.com/auth0/auth0-python/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. + required: true + - label: I have looked into the [API documentation](https://auth0-python.readthedocs.io/en/latest/) and have not found a suitable solution or answer. + required: true + - label: I have searched the [issues](https://github.com/auth0/auth0-python/issues) and have not found a suitable solution or answer. + required: true + - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. + required: true + - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). + required: true + + - type: textarea + id: description + attributes: + label: Description + description: Provide a clear and concise description of the issue, including what you expected to happen. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. + placeholder: | + 1. Step 1... + 2. Step 2... + 3. ... + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Other libraries that might be involved, or any other relevant information you think would be useful. + validations: + required: false + + - type: input + id: environment-version + attributes: + label: auth0-python version + validations: + required: true + + - type: input + id: environment-python-version + attributes: + label: Python version + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/Feature Request.yml b/.github/ISSUE_TEMPLATE/Feature Request.yml new file mode 100644 index 00000000..92745c12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature Request.yml @@ -0,0 +1,53 @@ +name: ๐Ÿงฉ Feature request +description: Suggest an idea or a feature for this library +labels: ["feature request"] + +body: + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have looked into the [Readme](https://github.com/auth0/auth0-python#readme) and [Examples](https://github.com/auth0/auth0-python/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. + required: true + - label: I have looked into the [API documentation](https://auth0-python.readthedocs.io/en/latest/) and have not found a suitable solution or answer. + required: true + - label: I have searched the [issues](https://github.com/auth0/auth0-python/issues) and have not found a suitable solution or answer. + required: true + - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. + required: true + - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). + required: true + + - type: textarea + id: description + attributes: + label: Describe the problem you'd like to have solved + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: ideal-solution + attributes: + label: Describe the ideal solution + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives-and-workarounds + attributes: + label: Alternatives and current workarounds + description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4cd71d48..65c99a9c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: false contact_links: - name: Auth0 Community - url: https://community.auth0.com/c/sdks/5 + url: https://community.auth0.com about: Discuss this SDK in the Auth0 Community forums - - name: Library Documentation - url: https://github.com/auth0/auth0-python/blob/master/README.rst - about: Read the library docs diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 68352ba2..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: Feature request -about: Suggest an idea or a feature for this project -title: '' -labels: feature request -assignees: '' ---- - - - -### Describe the problem you'd like to have solved - - - -### Describe the ideal solution - - - -## Alternatives and current work-arounds - - - -### Additional information, if any - - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md deleted file mode 100644 index 50b9fa7e..00000000 --- a/.github/ISSUE_TEMPLATE/report_a_bug.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: Report a bug -about: Have you found a bug or issue? Create a bug report for this SDK -title: '' -labels: bug report -assignees: '' ---- - - - -### Describe the problem - - - -### What was the expected behavior? - - - -### Reproduction - - -- Step 1.. -- Step 2.. -- ... - -### Environment - - - -- **Version of this library used:** -- **Which framework are you using, if applicable:** -- **Other modules/plugins/libraries that might be involved:** -- **Any other relevant information you think would be useful:** \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 92b54230..aa1a94f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,7 +19,7 @@ Please include relevant links supporting this change such as a: ### Testing -Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. +Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. - [ ] This change adds unit test coverage - [ ] This change adds integration test coverage diff --git a/.github/actions/get-prerelease/action.yml b/.github/actions/get-prerelease/action.yml new file mode 100644 index 00000000..131f93d1 --- /dev/null +++ b/.github/actions/get-prerelease/action.yml @@ -0,0 +1,30 @@ +name: Return a boolean indicating if the version contains prerelease identifiers + +# +# Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not. +# +# TODO: Remove once the common repo is public. +# + +inputs: + version: + required: true + +outputs: + prerelease: + value: ${{ steps.get_prerelease.outputs.PRERELEASE }} + +runs: + using: composite + + steps: + - id: get_prerelease + shell: bash + run: | + if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then + echo "PRERELEASE=true" >> $GITHUB_OUTPUT + else + echo "PRERELEASE=false" >> $GITHUB_OUTPUT + fi + env: + VERSION: ${{ inputs.version }} \ No newline at end of file diff --git a/.github/actions/get-release-notes/action.yml b/.github/actions/get-release-notes/action.yml new file mode 100644 index 00000000..5ce3f92e --- /dev/null +++ b/.github/actions/get-release-notes/action.yml @@ -0,0 +1,42 @@ +name: Return the release notes extracted from the PR body + +# +# Returns the release notes from the content of a pull request linked to a release branch. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc. +# +# TODO: Remove once the common repo is public. +# +inputs: + version: + required: true + repo_name: + required: false + repo_owner: + required: true + token: + required: true + +outputs: + release-notes: + value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} + +runs: + using: composite + + steps: + - uses: actions/github-script@v7 + id: get_release_notes + with: + result-encoding: string + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: process.env.REPO_OWNER, + repo: process.env.REPO_NAME, + state: 'all', + head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, + }); + core.setOutput('RELEASE_NOTES', pulls[0].body); + env: + GITHUB_TOKEN: ${{ inputs.token }} + REPO_OWNER: ${{ inputs.repo_owner }} + REPO_NAME: ${{ inputs.repo_name }} + VERSION: ${{ inputs.version }} \ No newline at end of file diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml new file mode 100644 index 00000000..84814a39 --- /dev/null +++ b/.github/actions/get-version/action.yml @@ -0,0 +1,21 @@ +name: Return the version extracted from the branch name + +# +# Returns the version from the .version file. +# +# TODO: Remove once the common repo is public. +# + +outputs: + version: + value: ${{ steps.get_version.outputs.VERSION }} + +runs: + using: composite + + steps: + - id: get_version + shell: bash + run: | + VERSION=$(head -1 .version) + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/actions/release-create/action.yml b/.github/actions/release-create/action.yml new file mode 100644 index 00000000..a0db443d --- /dev/null +++ b/.github/actions/release-create/action.yml @@ -0,0 +1,47 @@ +name: Create a GitHub release + +# +# Creates a GitHub release with the given version. +# +# TODO: Remove once the common repo is public. +# + +inputs: + token: + required: true + files: + required: false + name: + required: true + body: + required: true + tag: + required: true + commit: + required: true + draft: + default: false + required: false + prerelease: + default: false + required: false + fail_on_unmatched_files: + default: true + required: false + +runs: + using: composite + + steps: + - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + with: + body: ${{ inputs.body }} + name: ${{ inputs.name }} + tag_name: ${{ inputs.tag }} + target_commitish: ${{ inputs.commit }} + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} + files: ${{ inputs.files }} + env: + GITHUB_TOKEN: ${{ inputs.token }} \ No newline at end of file diff --git a/.github/actions/rl-scanner/action.yml b/.github/actions/rl-scanner/action.yml new file mode 100644 index 00000000..7a2b7746 --- /dev/null +++ b/.github/actions/rl-scanner/action.yml @@ -0,0 +1,71 @@ +name: "Reversing Labs Scanner" +description: "Runs the Reversing Labs scanner on a specified artifact." +inputs: + artifact-path: + description: "Path to the artifact to be scanned." + required: true + version: + description: "Version of the artifact." + required: true + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Python dependencies + shell: bash + run: | + pip install boto3 requests + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} + aws-region: us-east-1 + mask-aws-account-id: true + + - name: Install RL Wrapper + shell: bash + run: | + pip install rl-wrapper>=1.0.6 --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python-local/simple" + + - name: Run RL Scanner + shell: bash + env: + RLSECURE_LICENSE: ${{ env.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ env.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ env.SIGNAL_HANDLER_TOKEN }} + PYTHONUNBUFFERED: 1 + run: | + if [ ! -f "${{ inputs.artifact-path }}" ]; then + echo "Artifact not found: ${{ inputs.artifact-path }}" + exit 1 + fi + + rl-wrapper \ + --artifact "${{ inputs.artifact-path }}" \ + --name "${{ github.event.repository.name }}" \ + --version "${{ inputs.version }}" \ + --repository "${{ github.repository }}" \ + --commit "${{ github.sha }}" \ + --build-env "github_actions" \ + --suppress_output + + # Check the outcome of the scanner + if [ $? -ne 0 ]; then + echo "RL Scanner failed." + echo "scan-status=failed" >> $GITHUB_ENV + exit 1 + else + echo "RL Scanner passed." + echo "scan-status=success" >> $GITHUB_ENV + fi + +outputs: + scan-status: + description: "The outcome of the scan process." + value: ${{ env.scan-status }} diff --git a/.github/actions/tag-exists/action.yml b/.github/actions/tag-exists/action.yml new file mode 100644 index 00000000..b8f33f6a --- /dev/null +++ b/.github/actions/tag-exists/action.yml @@ -0,0 +1,36 @@ +name: Return a boolean indicating if a tag already exists for the repository + +# +# Returns a simple true/false boolean indicating whether the tag exists or not. +# +# TODO: Remove once the common repo is public. +# + +inputs: + token: + required: true + tag: + required: true + +outputs: + exists: + description: 'Whether the tag exists or not' + value: ${{ steps.tag-exists.outputs.EXISTS }} + +runs: + using: composite + + steps: + - id: tag-exists + shell: bash + run: | + GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" + http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") + if [ "$http_status_code" -ne "404" ] ; then + echo "EXISTS=true" >> $GITHUB_OUTPUT + else + echo "EXISTS=false" >> $GITHUB_OUTPUT + fi + env: + TAG_NAME: ${{ inputs.tag }} + GITHUB_TOKEN: ${{ inputs.token }} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..6e7f8d40 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/stale.yml b/.github/stale.yml index b2e13fc7..3cc35f17 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -17,4 +17,4 @@ staleLabel: closed:stale # Comment to post when marking as stale. Set to `false` to disable markComment: > - This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! ๐Ÿ™‡โ€โ™‚๏ธ \ No newline at end of file + This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! ๐Ÿ™‡โ€โ™‚๏ธ diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..d2183bca --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,18 @@ +name: Claude Code PR Review + +on: + issue_comment: + types: [ created ] + pull_request_review_comment: + types: [ created ] + pull_request_review: + types: [ submitted ] + +jobs: + claude-review: + permissions: + contents: write + issues: write + pull-requests: write + id-token: write + uses: auth0/auth0-ai-pr-analyzer-gh-action/.github/workflows/claude-code-review.yml@main \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..b8d64fc9 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,53 @@ +name: CodeQL + +on: + merge_group: + pull_request: + types: + - opened + - synchronize + push: + branches: + - master + schedule: + - cron: "56 12 * * 1" + +permissions: + actions: read + contents: read + security-events: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + analyze: + name: Check for Vulnerabilities + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [python] + + steps: + - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' + run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. + + - name: Checkout + uses: actions/checkout@v5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..6f5ba53e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,59 @@ +name: Build Documentation + +on: + push: + branches: + - master + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "documentation" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Configure Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Configure dependencies + run: | + pip install --user --upgrade pip + pip install --user pipx + pipx ensurepath + pipx install sphinx==5.3.0 + pipx inject sphinx pyjwt cryptography sphinx-mdinclude sphinx-rtd-theme sphinx-autodoc-typehints + + - name: Build documentation + run: | + sphinx-build ./docs/source ./docs/build --keep-going -n -a -b html + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: "./docs/build" + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: "github-pages" + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - id: deployment + name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..671b6efe --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,90 @@ +name: Publish Release + +on: + workflow_dispatch: + +### TODO: Replace instances of './.github/actions/' with reference to the `dx-sdk-actions` repo is made public and this file is transferred over +### TODO: Also remove `get-prerelease`, `get-version`, `release-create`, `tag-create` and `tag-exists` actions from this repo's .github/actions folder once the repo is public. + +permissions: + contents: write + id-token: write # Required for trusted publishing to PyPI + +jobs: + rl-scanner: + uses: ./.github/workflows/rl-scanner.yml + with: + python-version: "3.10" + artifact-name: "auth0-python.tgz" + secrets: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} + publish-pypi: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + name: "PyPI" + runs-on: ubuntu-latest + needs: rl-scanner + environment: release + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + + # Get the version from the branch name + - id: get_version + uses: ./.github/actions/get-version + + # Get the prerelease flag from the branch name + - id: get_prerelease + uses: ./.github/actions/get-prerelease + with: + version: ${{ steps.get_version.outputs.version }} + + # Get the release notes + # This will expose the release notes as env.RELEASE_NOTES + - id: get_release_notes + uses: ./.github/actions/get-release-notes + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ steps.get_version.outputs.version }} + repo_owner: ${{ github.repository_owner }} + repo_name: ${{ github.event.repository.name }} + + # Create a release for the tag + - uses: ./.github/actions/release-create + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: ${{ steps.get_version.outputs.version }} + body: ${{ steps.get_release_notes.outputs.release-notes }} + tag: ${{ steps.get_version.outputs.version }} + commit: ${{ github.sha }} + prerelease: ${{ steps.get_prerelease.outputs.prerelease }} + + - name: Configure Python + uses: actions/setup-python@v6 + with: + python-version: "3.9" + + - name: Configure dependencies + run: | + pip install --user --upgrade pip + pip install --user pipx + pipx ensurepath + pipx install poetry + poetry config virtualenvs.in-project true + poetry install --with dev + poetry self add "poetry-dynamic-versioning[plugin]" + + - name: Build release + run: | + poetry build + + - name: Publish release + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/rl-scanner.yml b/.github/workflows/rl-scanner.yml new file mode 100644 index 00000000..14514656 --- /dev/null +++ b/.github/workflows/rl-scanner.yml @@ -0,0 +1,83 @@ +name: RL-Secure Workflow + +on: + workflow_call: + inputs: + python-version: + required: true + type: string + artifact-name: + required: true + type: string + secrets: + RLSECURE_LICENSE: + required: true + RLSECURE_SITE_KEY: + required: true + SIGNAL_HANDLER_TOKEN: + required: true + PRODSEC_TOOLS_USER: + required: true + PRODSEC_TOOLS_TOKEN: + required: true + PRODSEC_TOOLS_ARN: + required: true + +jobs: + rl-scanner: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + outputs: + scan-status: ${{ steps.rl-scan-conclusion.outcome }} + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Configure Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Configure dependencies + run: | + pip install --user --upgrade pip + pip install --user pipx + pipx ensurepath + pipx install poetry==1.4.2 + pip install --upgrade pip + pip install boto3 requests + poetry config virtualenvs.in-project true + poetry install --with dev + poetry self add "poetry-dynamic-versioning[plugin]==1.1.1" + + - name: Build release + run: | + poetry build + + - name: Create tgz build artifact + run: | + tar -czvf ${{ inputs.artifact-name }} * + + - name: Get Artifact Version + id: get_version + uses: ./.github/actions/get-version + + - name: Run RL Scanner + id: rl-scan-conclusion + uses: ./.github/actions/rl-scanner + with: + artifact-path: "$(pwd)/${{ inputs.artifact-name }}" + version: "${{ steps.get_version.outputs.version }}" + env: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} + + - name: Output scan result + run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 00000000..3bfb147e --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,40 @@ +name: Snyk + +on: + merge_group: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + push: + branches: + - master + schedule: + - cron: '30 0 1,15 * *' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + + check: + + name: Check for Vulnerabilities + runs-on: ubuntu-latest + + steps: + - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' + run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. + + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - uses: snyk/actions/python@9adf32b1121593767fc3c057af55b55db032dc04 # pin@1.0.0 + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..89f36e31 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,85 @@ +name: Build and Test + +on: + merge_group: + pull_request: + types: + - opened + - synchronize + push: + branches: + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + run: + name: Run + runs-on: ubuntu-latest + + env: + BUBBLEWRAP_ARGUMENTS: | + --unshare-all \ + --clearenv \ + --ro-bind / / \ + --bind ${{ github.workspace }} ${{ github.workspace }} \ + --tmpfs $HOME \ + --tmpfs /tmp \ + --tmpfs /var \ + --dev /dev \ + --proc /proc \ + --die-with-parent \ + --new-session \ + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - name: Configure Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: "${{ matrix.python-version }}" + + - name: Configure dependencies + run: | + sudo apt install bubblewrap + pip install --user --upgrade pip + pip install --user pipx + pip install --user setuptools + pipx ensurepath + pipx install poetry + poetry config virtualenvs.in-project true + poetry install --with dev + poetry self add "poetry-dynamic-versioning[plugin]" + + - name: Run tests + run: | + poetry run pytest --cov=auth0 --cov-report=term-missing:skip-covered --cov-report=xml + + # - name: Run lint + # run: | + # pipx install black==23.3.0 + # pipx install flake8==5.0.4 + # pipx install isort==5.11.5 + # pipx install pyupgrade==3.3.2 + # black . --check + # flake8 . --count --show-source --statistics + # isort . --diff --profile black + # pyupgrade . --py37-plus --keep-runtime-typing + + - if: ${{ matrix.python-version == '3.10' }} + name: Upload coverage + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # pin@5.5.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/v5-ci.yml b/.github/workflows/v5-ci.yml new file mode 100644 index 00000000..90a099a8 --- /dev/null +++ b/.github/workflows/v5-ci.yml @@ -0,0 +1,43 @@ +name: Test (v5) + +on: + push: + branches: + - v5 + pull_request: + branches: + - v5 +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Test + run: poetry run pytest -rP . diff --git a/.github/workflows/v5-publish.yml b/.github/workflows/v5-publish.yml new file mode 100644 index 00000000..82b8b80f --- /dev/null +++ b/.github/workflows/v5-publish.yml @@ -0,0 +1,109 @@ +name: Publish Release (v5) + +on: + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + type: choice + required: true + options: + - testpypi + - pypi + default: 'testpypi' + pull_request: + branches: + - v5 + +permissions: + contents: write + id-token: write + +jobs: + publish-pypi: + name: "PyPI" + runs-on: ubuntu-latest + environment: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || 'testpypi' }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + + - id: get_version + name: Get version from pyproject.toml + run: | + VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + - id: get_prerelease + name: Determine if pre-release + run: | + VERSION="${{ steps.get_version.outputs.version }}" + if [[ "$VERSION" =~ (a|alpha|b|beta|rc)[0-9]* ]]; then + echo "prerelease=true" >> $GITHUB_OUTPUT + echo "Pre-release: true" + else + echo "prerelease=false" >> $GITHUB_OUTPUT + echo "Stable release" + fi + + - name: Create GitHub Release + if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'pypi' + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.get_version.outputs.version }} + release_name: v${{ steps.get_version.outputs.version }} + body: | + See [v5_MIGRATION_GUIDE.md](https://github.com/${{ github.repository }}/blob/main/v5_MIGRATION_GUIDE.md) for migration instructions. + draft: false + prerelease: ${{ steps.get_prerelease.outputs.prerelease }} + + - name: Configure Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Configure dependencies + run: | + pip install --user --upgrade pip + pip install --user pipx + pipx ensurepath + pipx install poetry + poetry config virtualenvs.in-project true + poetry install + + - name: Build release + run: | + poetry build + ls -lh dist/ + echo "Build successful! Artifacts created:" + ls -lh dist/ + + - name: Publish to Test PyPI + if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'testpypi' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + print-hash: true + + - name: Publish to PyPI + if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'pypi' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + print-hash: true + + - name: Summary + run: | + echo "### Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Version: ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- Environment: ${{ github.event.inputs.environment }}" >> $GITHUB_STEP_SUMMARY + echo "- Pre-release: ${{ steps.get_prerelease.outputs.prerelease }}" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 25c2766b..d52ba118 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,13 @@ lib64/ parts/ sdist/ var/ +bin/ *.egg-info/ .installed.cfg *.egg .pypirc +pyvenv.cfg +.python-version # Installer logs pip-log.txt @@ -50,7 +53,7 @@ docs/build/ # IDEA .idea/ -*.iml +*.iml # VSCode .vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 764d539c..a598fec7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,37 @@ -- repo: https://github.com/asottile/pyupgrade - rev: v1.6.1 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 hooks: - - id: pyupgrade + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.2 + hooks: + - id: pyupgrade + args: [--keep-runtime-typing] + + - repo: https://github.com/pycqa/isort + rev: 5.11.5 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + + - repo: https://github.com/python-poetry/poetry + rev: 1.4.2 + hooks: + - id: poetry-check + - id: poetry-lock + - id: poetry-export + args: ["--with", "dev", "--without-hashes", "--format", "requirements.txt", "--output", "requirements.txt"] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8e0743de..b27cfe14 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,4 +6,4 @@ sphinx: python: version: "3.7" install: - - requirements: docs/requirements.txt \ No newline at end of file + - requirements: requirements.txt diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..f37bda94 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,6 @@ +/.github/ +/docs/ +/examples/ +/auth0/test/ +/auth0/test_asyc/ +*.md diff --git a/.shiprc b/.shiprc new file mode 100644 index 00000000..ce24dbdd --- /dev/null +++ b/.shiprc @@ -0,0 +1,6 @@ +{ + "files": { + ".version": [] + }, + "prefixVersion": false +} diff --git a/.snyk b/.snyk index 3b39db80..785d93ea 100644 --- a/.snyk +++ b/.snyk @@ -9,4 +9,8 @@ ignore: SNYK-PYTHON-REQUESTS-40470: - '*': reason: 'patched in latest python versions: https://bugs.python.org/issue27568' + "snyk:lic:pip:certifi:MPL-2.0": + - '*': + reason: "Accepting certifiโ€™s MPL-2.0 license for now" + expires: "2030-12-31T23:59:59Z" patch: {} diff --git a/.version b/.version new file mode 100644 index 00000000..01b73abe --- /dev/null +++ b/.version @@ -0,0 +1 @@ +4.13.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8c35ea..2cd01db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,221 @@ -Changes -======= +# Change Log + +## [4.13.0](https://github.com/auth0/auth0-python/tree/4.13.0) (2025-09-17) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.12.0...4.13.0) + +**Added** +- fix(backchannel): expose headers on `slow_down` errors (HTTP 429s) [\#744](https://github.com/auth0/auth0-python/pull/744) ([pmalouin](https://github.com/pmalouin)) + +## [4.12.0](https://github.com/auth0/auth0-python/tree/4.12.0) (2025-09-15) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.11.0...4.12.0) + +**Added** +- Updates for CIBA with email [\#720](https://github.com/auth0/auth0-python/pull/720) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.11.0](https://github.com/auth0/auth0-python/tree/4.11.0) (2025-09-11) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.10.0...4.11.0) + +**Added** +- feat: Support For Network ACL Endpoints [\#706](https://github.com/auth0/auth0-python/pull/706) ([kishore7snehil](https://github.com/kishore7snehil)) + +**Fixed** +- chore: fix workflow syntax errors and update dependencies [\#724](https://github.com/auth0/auth0-python/pull/724) ([kishore7snehil](https://github.com/kishore7snehil)) + +## [4.10.0](https://github.com/auth0/auth0-python/tree/4.10.0) (2025-06-05) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.9.0...4.10.0) + +**Added** +- chore: merge community PRs โ€“ bugfixes, features, and dependency upgrades [\#696](https://github.com/auth0/auth0-python/pull/696) ([kishore7snehil](https://github.com/kishore7snehil)) + +**Fixed** +- fix: handle `authorization_details` in back_channel_login [\#695](https://github.com/auth0/auth0-python/pull/695) ([kishore7snehil](https://github.com/kishore7snehil)) + +## [4.9.0](https://github.com/auth0/auth0-python/tree/4.9.0) (2025-04-01) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.8.1...4.9.0) + +**Added** +- feat: Federated Connections Support [\#682](https://github.com/auth0/auth0-python/pull/682) ([kishore7snehil](https://github.com/kishore7snehil)) +- Adding Support For CIBA with RAR [\#679](https://github.com/auth0/auth0-python/pull/679) ([kishore7snehil](https://github.com/kishore7snehil)) + +## [4.8.1](https://github.com/auth0/auth0-python/tree/4.8.1) (2025-02-24) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.8.0...4.8.1) + +**Fixed** +- Fix: Unauthorized Access Error For PAR [\#671](https://github.com/auth0/auth0-python/pull/671) ([kishore7snehil](https://github.com/kishore7snehil)) + +## [4.8.0](https://github.com/auth0/auth0-python/tree/4.8.0) (2025-01-29) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.7.2...4.8.0) + +**Added** +- Adding Support For RAR and JAR Requests [\#659](https://github.com/auth0/auth0-python/pull/659) ([kishore7snehil](https://github.com/kishore7snehil)) +- Adding Support For Back Channel Login [\#643](https://github.com/auth0/auth0-python/pull/643) ([kishore7snehil](https://github.com/kishore7snehil)) + +**Fixed** +- Consolidated Community PRs and Dependency Upgrades [\#660](https://github.com/auth0/auth0-python/pull/660) ([kishore7snehil](https://github.com/kishore7snehil)) + - [fix typo in docstring](https://github.com/auth0/auth0-python/pull/637) ([@CarlosEduR ](https://github.com/CarlosEduR)) + - [Added support for "include_totals" to all_organization_member_roles](https://github.com/auth0/auth0-python/pull/635) ([@jpayton-cx](https://github.com/jpayton-cx)) + - [Fixed Version Table](https://github.com/auth0/auth0-python/pull/633) ([@sanchez](https://github.com/sanchez)) + - [Remove upper bounds on all python dependency versions](https://github.com/auth0/auth0-python/pull/628) ([@ngfeldman](https://github.com/ngfeldman)) + - [Adding secrets to Codecov Action Upload](https://github.com/auth0/auth0-python/pull/624) ([@developerkunal](https://github.com/developerkunal)) +- Updating Dependancies And Workflow Action Versions [\#653](https://github.com/auth0/auth0-python/pull/653) ([kishore7snehil](https://github.com/kishore7snehil)) +- Fixing the Github Workflow Issues [\#644](https://github.com/auth0/auth0-python/pull/644) ([kishore7snehil](https://github.com/kishore7snehil)) + +## [4.7.2](https://github.com/auth0/auth0-python/tree/4.7.2) (2024-09-10) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.7.1...4.7.2) + +**Security** +- Update cryptography requirements.txt [\#630](https://github.com/auth0/auth0-python/pull/630) ([duedares-rvj](https://github.com/duedares-rvj)) + +## [4.7.1](https://github.com/auth0/auth0-python/tree/4.7.1) (2024-02-26) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.7.0...4.7.1) + +**Security** +- Update cryptography requirements.txt [\#597](https://github.com/auth0/auth0-python/pull/597) ([skjensen](https://github.com/skjensen)) + +## [4.7.0](https://github.com/auth0/auth0-python/tree/4.7.0) (2023-12-05) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.6.1...4.7.0) + +**โš ๏ธ BREAKING CHANGES** +- Add python 3.12 support, drop 3.7 [\#562](https://github.com/auth0/auth0-python/pull/562) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +**Added** +- [SDK-4138] Add support for Pushed Authorization Requests (PAR) [\#560](https://github.com/auth0/auth0-python/pull/560) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.6.1](https://github.com/auth0/auth0-python/tree/4.6.1) (2023-11-29) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.6.0...4.6.1) + +**Fixed** +- Fix rest_async and async tests [\#556](https://github.com/auth0/auth0-python/pull/556) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- fix type hint for link_user_account [\#552](https://github.com/auth0/auth0-python/pull/552) ([tzzh](https://github.com/tzzh)) + +## [4.6.0](https://github.com/auth0/auth0-python/tree/4.6.0) (2023-11-09) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.5.0...4.6.0) + +**Added** +- [SDK-4544] Add orgs in client credentials support [\#549](https://github.com/auth0/auth0-python/pull/549) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Authentication API, the Database classs, Add the organization param to the change_password method [\#539](https://github.com/auth0/auth0-python/pull/539) ([shchotse](https://github.com/shchotse)) +- Retry all methods on 429 [\#518](https://github.com/auth0/auth0-python/pull/518) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.5.0](https://github.com/auth0/auth0-python/tree/4.5.0) (2023-10-20) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.4.2...4.5.0) + +**Added** +- [SDK-4656] Add fields to all_organization_members [\#537](https://github.com/auth0/auth0-python/pull/537) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.4.2](https://github.com/auth0/auth0-python/tree/4.4.2) (2023-08-31) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.4.1...4.4.2) + +**Fixed** +- Fix python dependency version [\#522](https://github.com/auth0/auth0-python/pull/522) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Revert publishing types [\#521](https://github.com/auth0/auth0-python/pull/521) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.4.1](https://github.com/auth0/auth0-python/tree/4.4.1) (2023-08-21) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.4.0...4.4.1) + +**Fixed** +- Fix for async types [\#515](https://github.com/auth0/auth0-python/pull/515) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.4.0](https://github.com/auth0/auth0-python/tree/4.4.0) (2023-07-25) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.3.0...4.4.0) + +**Added** +- [SDK-4394] Add organization name validation [\#507](https://github.com/auth0/auth0-python/pull/507) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add type hints to `management` [\#497](https://github.com/auth0/auth0-python/pull/497) ([Viicos](https://github.com/Viicos)) + +**Fixed** +- Fix asyncify for users client where token is not required [\#506](https://github.com/auth0/auth0-python/pull/506) ([cgearing](https://github.com/cgearing)) + +## [4.3.0](https://github.com/auth0/auth0-python/tree/4.3.0) (2023-06-26) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.2.0...4.3.0) + +**Added** +- Add forwardedFor option to password grant login [\#501](https://github.com/auth0/auth0-python/pull/501) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add connections.all name parameter [\#500](https://github.com/auth0/auth0-python/pull/500) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add type hints to base and `authentication` [\#472](https://github.com/auth0/auth0-python/pull/472) ([Viicos](https://github.com/Viicos)) + +**Fixed** +- Fix async auth client [\#499](https://github.com/auth0/auth0-python/pull/499) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Fix update_template_universal_login [\#495](https://github.com/auth0/auth0-python/pull/495) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.2.0](https://github.com/auth0/auth0-python/tree/4.2.0) (2023-05-02) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.1.1...4.2.0) + +**Added** +- Add cache_ttl param to AsymmetricSignatureVerifier [\#490](https://github.com/auth0/auth0-python/pull/490) ([matei-radu](https://github.com/matei-radu)) + +## [4.1.1](https://github.com/auth0/auth0-python/tree/4.1.1) (2023-04-13) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.1.0...4.1.1) + +**Fixed** +- Make pw realm params optional [\#484](https://github.com/auth0/auth0-python/pull/484) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Fix intellisense on Auth0 class [\#486](https://github.com/auth0/auth0-python/pull/486) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [4.1.0](https://github.com/auth0/auth0-python/tree/4.1.0) (2023-03-14) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.0.0...4.1.0) + +**Added** +- Add branding theme endpoints [\#477](https://github.com/auth0/auth0-python/pull/477) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- [SDK-4011] Add API2 Factor Management Endpoints [\#476](https://github.com/auth0/auth0-python/pull/476) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Use declarative setup with `pyproject.toml` [\#474](https://github.com/auth0/auth0-python/pull/474) ([Viicos](https://github.com/Viicos)) + +## [4.0.0](https://github.com/auth0/auth0-python/tree/4.0.0) (2023-01-19) +[Full Changelog](https://github.com/auth0/auth0-python/compare/3.24.1...4.0.0) + +**Added** +- Add support for private_key_jwt [\#456](https://github.com/auth0/auth0-python/pull/456) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add support for managing client credentials [\#459](https://github.com/auth0/auth0-python/pull/459) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +**Security** +- Update pyjwt [\#460](https://github.com/auth0/auth0-python/pull/460) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +**Changed** +- Publish Python Support Schedule [\#454](https://github.com/auth0/auth0-python/pull/454) ([evansims](https://github.com/evansims)) + +**โš ๏ธ BREAKING CHANGES** +- Remove deprecated methods [\#461](https://github.com/auth0/auth0-python/pull/461) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Remove v3 folder [\#462](https://github.com/auth0/auth0-python/pull/462) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +See the [V4_MIGRATION_GUIDE](https://github.com/auth0/auth0-python/blob/master/V4_MIGRATION_GUIDE.md) for more info. + +## [3.24.1](https://github.com/auth0/auth0-python/tree/3.24.1) (2023-01-19) +[Full Changelog](https://github.com/auth0/auth0-python/compare/3.24.0...3.24.1) + +**Fixed** +- Remove unnecessary type param from update_template_universal_login [\#463](https://github.com/auth0/auth0-python/pull/463) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [3.24.0](https://github.com/auth0/auth0-python/tree/3.24.0) (2022-10-17) +[Full Changelog](https://github.com/auth0/auth0-python/compare/3.23.1...3.24.0) + +**Added** +- [SDK-3714] Async token verifier [\#445](https://github.com/auth0/auth0-python/pull/445) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add AsyncAuth0 to share a session among many services [\#443](https://github.com/auth0/auth0-python/pull/443) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +**Fixed** +- Bugfix 414 missing import [\#442](https://github.com/auth0/auth0-python/pull/442) ([adamjmcgrath](https://github.com/adamjmcgrath)) + +## [3.23.1](https://github.com/auth0/auth0-python/tree/3.23.1) (2022-06-10) +[Full Changelog](https://github.com/auth0/auth0-python/compare/3.23.0...3.23.1) + +**Fixed** +- Pass rest_options through Auth0 constructor [\#354](https://github.com/auth0/auth0-python/pull/354) ([adamjmcgrath](https://github.com/adamjmcgrath)) + + +3.23.0 +------------------ + +**Added** +- Asyncio Support [\#312](https://github.com/auth0/auth0-python/pull/312) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Add `/api/v2/branding` endpoints support [\#313](https://github.com/auth0/auth0-python/pull/313) ([evansims](https://github.com/evansims)) + +3.22.0 +------------------ + +**Added** +- [SDK-3174] Add `DELETE` method for `/api/v2/users/{id}/authenticators` endpoint [\#301](https://github.com/auth0/auth0-python/pull/301) ([akmjenkins](https://github.com/akmjenkins)) +- [SDK-3175] Return token claims in TokenVerifier.verify() [\#273](https://github.com/auth0/auth0-python/pull/273) ([bisguzar](https://github.com/bisguzar)) + +**Fixed** +- [SDK-3173] Default to 'None' for `deployed` on GET /api/v2/actions/actions endpoint [\#309](https://github.com/auth0/auth0-python/pull/309) ([evansims](https://github.com/evansims)) 3.21.0 ------------------ @@ -142,7 +358,7 @@ Changes 3.8.1 ------------------ -July 18, 2019: This release included an unintentionally breaking change affecting those users that were manually parsing the response from GET requests. e.g. /userinfo or /authorize. The `AuthenticationBase#get` method was incorrectly parsing the request result into a String. +July 18, 2019: This release included an unintentionally breaking change affecting those users that were manually parsing the response from GET requests. e.g. /userinfo or /authorize. The `AuthenticationBase#get` method was incorrectly parsing the request result into a String. From this release on, making a GET request returns a Dictionary instead. @@ -274,7 +490,7 @@ Authentication API Authentication API - Added Logout Functionality -3.0.0 +3.0.0 ------------------ Authentication API @@ -287,8 +503,8 @@ Authentication API - Authorization Code Grant Management API v2 -- Added Support for Guardian +- Added Support for Guardian - Added Support to retrieve Logs - Added Support to manage Resource Servers - Added Support to manage Client Grants -- Added Support to manage User blocks +- Added Support to manage User blocks diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..959e0314 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,197 @@ +# Examples using auth0-python + +- [Authentication SDK](#authentication-sdk) + - [ID token validation](#id-token-validation) + - [Authenticating with a application configured to use `private_key_jwt` token endpoint auth method](#authenticating-with-a-application-configured-to-use-private-key-jwt-token-endpoint-auth-method) +- [Management SDK](#management-sdk) + - [Connections](#connections) +- [Error handling](#error-handling) +- [Asynchronous environments](#asynchronous-environments) + +## Authentication SDK + +### ID token validation + +Upon successful authentication, the credentials received may include an `id_token`, if the authentication request contained the `openid` scope. The `id_token` contains information associated with the authenticated user. You can read more about ID tokens [here](https://auth0.com/docs/tokens/concepts/id-tokens). + +Before you access its contents, you must verify that the ID token has not been tampered with and that it is meant for your application to consume. The `TokenVerifier` class can be used to perform this verification. + +To create a `TokenVerifier`, the following arguments are required: + +- A `SignatureVerifier` instance, which is responsible for verifying the token's algorithm name and signature. +- The expected issuer value, which typically matches the Auth0 domain prefixed with `https://` and suffixed with `/`. +- The expected audience value, which typically matches the Auth0 application client ID. + +The type of `SignatureVerifier` used depends upon the signing algorithm used by your Auth0 application. You can view this value in your application settings under `Advanced settings | OAuth | JsonWebToken Signature Algorithm`. Auth0 recommends using the RS256 asymmetric signing algorithm. You can read more about signing algorithms [here](https://auth0.com/docs/tokens/signing-algorithms). + +For asymmetric algorithms like RS256, use the `AsymmetricSignatureVerifier` class, passing +the public URL where the certificates for the public keys can be found. This will typically be your Auth0 domain with the `/.well-known/jwks.json` path appended to it. For example, `https://your-domain.auth0.com/.well-known/jwks.json`. + +For symmetric algorithms like HS256, use the `SymmetricSignatureVerifier` class, passing the value of the client secret of your Auth0 application. + +The following example demonstrates the verification of an ID token signed with the RS256 signing algorithm: + +```python +from auth0.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier + +domain = 'myaccount.auth0.com' +client_id = 'exampleid' + +# After authenticating +id_token = auth_result['id_token'] + +jwks_url = 'https://{}/.well-known/jwks.json'.format(domain) +issuer = 'https://{}/'.format(domain) + +sv = AsymmetricSignatureVerifier(jwks_url) # Reusable instance +tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id) +tv.verify(id_token) +``` + +If the token verification fails, a `TokenValidationError` will be raised. In that scenario, the ID token should be deemed invalid and its contents should not be trusted. + +### Authenticating with a application configured to use `private_key_jwt` token endpoint auth method + +```python +from auth0.authentication import GetToken + +private_key = """-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAwfUb0nUC0aKB3WiytFhnCIg455BYC+dR3MUGadWpIg7S6lbi +... +2tjIvH4GN9ZkIGwzxIOP61wkUGwGaIzacOTIWOvqRI0OaYr9U18Ep1trvgGR +-----END RSA PRIVATE KEY----- +""" + +get_token = GetToken( + "my-domain.us.auth0.com", + "my-client-id", + client_assertion_signing_key=private_key, +) +token = get_token.client_credentials( + "https://my-domain.us.auth0.com/api/v2/" +) +``` + +## Management SDK + +### Connections + +Let's see how we can use this to get all available connections. +(this action requires the token to have the following scope: `read:connections`) + +```python +auth0.connections.all() +``` + +Which will yield a list of connections similar to this: + +```python +[ + { + 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], + 'id': u'con_ErZf9LpXQDE0cNBr', + 'name': u'Amazon-Connection', + 'options': {u'profile': True, u'scope': [u'profile']}, + 'strategy': u'amazon' + }, + { + 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], + 'id': u'con_i8qF5DPiZ3FdadwJ', + 'name': u'Username-Password-Authentication', + 'options': {u'brute_force_protection': True}, + 'strategy': u'auth0' + } +] +``` + +Modifying an existing connection is equally as easy. Let's change the name +of connection `'con_ErZf9LpXQDE0cNBr'`. +(The token will need scope: `update:connections` to make this one work) + +```python +auth0.connections.update('con_ErZf9LpXQDE0cNBr', {'name': 'MyNewName'}) +``` + +That's it! Using the `get` method of the connections endpoint we can verify +that the rename actually happened. + +```python +modified_connection = auth0.connections.get('con_ErZf9LpXQDE0cNBr') +``` + +Which returns something like this + +```python +{ + 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], + 'id': u'con_ErZf9LpXQDE0cNBr', + 'name': u'MyNewName', + 'options': {u'profile': True, u'scope': [u'profile']}, + 'strategy': u'amazon' +} +``` +Success! + +All endpoints follow a similar structure to `connections`, and try to follow as +closely as possible the [API documentation](https://auth0.com/docs/api/v2). + +## Error handling + +When consuming methods from the API clients, the requests could fail for a number of reasons: +- Invalid data sent as part of the request: An `Auth0Error` is raised with the error code and description. +- Global or Client Rate Limit reached: A `RateLimitError` is raised and the time at which the limit +resets is exposed in the `reset_at` property. When the header is unset, this value will be `-1`. +- Network timeouts: Adjustable by passing a `timeout` argument to the client. See the [rate limit docs](https://auth0.com/docs/policies/rate-limits) for details. + +## Asynchronous environments + +This SDK provides async methods built on top of [asyncio](https://docs.python.org/3/library/asyncio.html). To make them available you must have the [aiohttp](https://docs.aiohttp.org/en/stable/) module installed. + +Then additional methods with the `_async` suffix will be added to modules created by the `management.Auth0` class or to classes that are passed to the `asyncify` method. For example: + +```python +import asyncio +import aiohttp +from auth0.asyncify import asyncify +from auth0.management import Auth0, Users, Connections +from auth0.authentication import Users as AuthUsers + +auth0 = Auth0('domain', 'mgmt_api_token') + + +async def main(): + # users = auth0.users.all() <= sync + users = await auth0.users.all_async() # <= async + + # To share a session amongst multiple calls to the same service + async with auth0.users as users: + data = await users.get_async(id) + users.update_async(id, data) + + # To share a session amongst multiple calls to multiple services + async with Auth0('domain', 'mgmt_api_token') as auth0: + user = await auth0.users.get_async(user_id) + connection = await auth0.connections.get_async(connection_id) + + # Use asyncify directly on services + Users = asyncify(Users) + Connections = asyncify(Connections) + users = Users(domain, mgmt_api_token) + connections = Connections(domain, mgmt_api_token) + + # Create a session and share it among the services + session = aiohttp.ClientSession() + users.set_session(session) + connections.set_session(session) + u = await auth0.users.all_async() + c = await auth0.connections.all_async() + session.close() + + # Use auth api + U = asyncify(AuthUsers) + u = U(domain=domain) + await u.userinfo_async(access_token) + + +asyncio.run(main()) +``` diff --git a/LICENSE b/LICENSE index 586f3dbf..d6c52379 100644 --- a/LICENSE +++ b/LICENSE @@ -1,17 +1,17 @@ The MIT License (MIT) - + Copyright (c) 2017 Auth0, Inc. (http://auth0.com) - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/README.md b/README.md new file mode 100644 index 00000000..bf311868 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +![Auth0 SDK for Python](https://cdn.auth0.com/website/sdks/banners/auth0-python-banner.png) + +![Release](https://img.shields.io/pypi/v/auth0-python) +[![Codecov](https://img.shields.io/codecov/c/github/auth0/auth0-python)](https://codecov.io/gh/auth0/auth0-python) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/auth0/auth0-python) +![Downloads](https://img.shields.io/pypi/dw/auth0-python) +[![License](https://img.shields.io/:license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) +[![CircleCI](https://img.shields.io/circleci/build/github/auth0/auth0-python)](https://circleci.com/gh/auth0/auth0-python) + +
+๐Ÿ“š Documentation - ๐Ÿš€ Getting started - ๐Ÿ’ป API reference - ๐Ÿ’ฌ Feedback +
+ + +Learn how to integrate Auth0 with Python. +## Documentation +- [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0. + +## Getting started +### Installation +You can install the auth0 Python SDK using the following command. +``` +pip install auth0-python +``` + +> Requires Python 3.7 or higher. + +### Usage + +#### Authentication SDK +The Authentication SDK is organized into components that mirror the structure of the +[API documentation](https://auth0.com/docs/auth-api). + +If you need to sign up a user using their email and password, you can use the Database object. + +```python +from auth0.authentication import Database + +database = Database('my-domain.us.auth0.com', 'my-client-id') + +database.signup(email='user@domain.com', password='secr3t', connection='Username-Password-Authentication') +``` + +If you need to authenticate a user using their email and password, you can use the `GetToken` object, which enables making requests to the `/oauth/token` endpoint. + +```python +from auth0.authentication import GetToken + +token = GetToken('my-domain.us.auth0.com', 'my-client-id', client_secret='my-client-secret') + +token.login(username='user@domain.com', password='secr3t', realm='Username-Password-Authentication') +``` + +#### Management SDK +To use the management library you will need to instantiate an Auth0 object with a domain and a [Management API v2 token](https://auth0.com/docs/api/management/v2/tokens). Please note that these token last 24 hours, so if you need it constantly you should ask for it programmatically using the client credentials grant with a [non interactive client](https://auth0.com/docs/api/management/v2/tokens#1-create-and-authorize-a-client) authorized to access the API. For example: + +```python +from auth0.authentication import GetToken + +domain = 'myaccount.auth0.com' +non_interactive_client_id = 'exampleid' +non_interactive_client_secret = 'examplesecret' + +get_token = GetToken(domain, non_interactive_client_id, client_secret=non_interactive_client_secret) +token = get_token.client_credentials('https://{}/api/v2/'.format(domain)) +mgmt_api_token = token['access_token'] +``` + +Then use the token you've obtained as follows: + +```python +from auth0.management import Auth0 + +domain = 'myaccount.auth0.com' +mgmt_api_token = 'MGMT_API_TOKEN' + +auth0 = Auth0(domain, mgmt_api_token) +``` + +The `Auth0()` object is now ready to take orders, see our [connections example](https://github.com/auth0/auth0-python/blob/master/EXAMPLES.md#connections) to find out how to use it! + +For more code samples on how to integrate the auth0-python SDK in your Python application, have a look at our [examples](https://github.com/auth0/auth0-python/blob/master/EXAMPLES.md). + +## API reference + +### Authentication Endpoints + +- Database ( `authentication.Database` ) +- Delegated ( `authentication.Delegated` ) +- Enterprise ( `authentication.Enterprise` ) +- API Authorization - Get Token ( `authentication.GetToken`) +- BackChannelLogin ( `authentication.BackChannelLogin`) +- Passwordless ( `authentication.Passwordless` ) +- PushedAuthorizationRequests ( `authentication.PushedAuthorizationRequests` ) +- RevokeToken ( `authentication.RevokeToken` ) +- Social ( `authentication.Social` ) +- Users ( `authentication.Users` ) + + +### Management Endpoints + +- Actions() (`Auth0().action`) +- AttackProtection() (`Auth0().attack_protection`) +- Blacklists() ( `Auth0().blacklists` ) +- Branding() ( `Auth0().branding` ) +- ClientCredentials() ( `Auth0().client_credentials` ) +- ClientGrants() ( `Auth0().client_grants` ) +- Clients() ( `Auth0().clients` ) +- Connections() ( `Auth0().connections` ) +- CustomDomains() ( `Auth0().custom_domains` ) +- DeviceCredentials() ( `Auth0().device_credentials` ) +- EmailTemplates() ( `Auth0().email_templates` ) +- Emails() ( `Auth0().emails` ) +- Grants() ( `Auth0().grants` ) +- Guardian() ( `Auth0().guardian` ) +- Hooks() ( `Auth0().hooks` ) +- Jobs() ( `Auth0().jobs` ) +- LogStreams() ( `Auth0().log_streams` ) +- Logs() ( `Auth0().logs` ) +- NetworkAcls() ( `Auth0().network_acls` ) +- Organizations() ( `Auth0().organizations` ) +- Prompts() ( `Auth0().prompts` ) +- ResourceServers() (`Auth0().resource_servers` ) +- Roles() ( `Auth0().roles` ) +- RulesConfigs() ( `Auth0().rules_configs` ) +- Rules() ( `Auth0().rules` ) +- SelfServiceProfiles() ( `Auth0().self_service_profiles` ) +- Stats() ( `Auth0().stats` ) +- Tenants() ( `Auth0().tenants` ) +- Tickets() ( `Auth0().tickets` ) +- UserBlocks() (`Auth0().user_blocks` ) +- UsersByEmail() ( `Auth0().users_by_email` ) +- Users() ( `Auth0().users` ) + +## Support Policy + +Our support lifecycle policy mirrors the [Python support schedule](https://devguide.python.org/versions/). We do not support running the SDK on unsupported versions of Python that have ceased to receive security updates. Please ensure your environment remains up to date and running the latest Python version possible. + +| SDK Version | Python Version | Support Ends | +|-------------|----------------|--------------| +| 4.x | 3.12 | Oct 2028 | +| | 3.11 | Oct 2027 | +| | 3.10 | Oct 2026 | +| | 3.9 | Oct 2025 | +| | 3.8 | Oct 2024 | + +> As `pip` [reliably avoids](https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata) installing package updates that target incompatible Python versions, we may opt to remove support for [end-of-life](https://en.wikipedia.org/wiki/CPython#Version_history) Python versions during minor SDK updates. These are not considered breaking changes by this SDK. + +The following is a list of unsupported Python versions, and the last SDK version supporting them: + +| Python Version | Last SDK Version Supporting | +|----------------|-----------------------------| +| >= 3.7 | 4.6.1 | +| >= 2.0, <= 3.6 | 3.x | + +You can determine what version of Python you have installed by running: + +``` +python --version +``` + +## Feedback + +### Contributing + +We appreciate feedback and contribution to this repo! Before you get started, please see the following: + +- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) + +### Raise an issue + +To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-python/issues). + +### Vulnerability Reporting + +Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. + +--- + +

+ + + + Auth0 Logo + +

+

Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0?

+

+This project is licensed under the MIT license. See the LICENSE file for more info.

\ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 1989f159..00000000 --- a/README.rst +++ /dev/null @@ -1,441 +0,0 @@ -|pypi| |build| |coverage| |license| - -Learn how to integrate Auth0 with Python. - -===== -Usage -===== - -************ -Installation -************ - -You can install the auth0 Python SDK using the following command. - -.. code-block:: python - - pip install auth0-python - -For python3, use the following command - -.. code-block:: python - - pip3 install auth0-python - -Python 3.2 and 3.3 have reached `EOL `__ and support will be removed in the near future. - -****************** -Authentication SDK -****************** - -The Authentication SDK is organized into components that mirror the structure of the -`API documentation `__. -For example: - -.. code-block:: python - - from auth0.v3.authentication import Social - - social = Social('myaccount.auth0.com') - - social.login(client_id='...', access_token='...', connection='facebook') - - -If you need to sign up a user using their email and password, you can use the Database object. - -.. code-block:: python - - from auth0.v3.authentication import Database - - database = Database('myaccount.auth0.com'') - - database.signup(client_id='...', email='user@domain.com', password='secr3t', connection='Username-Password-Authentication') - - -If you need to authenticate a user using their email and password, you can use the ``GetToken`` object, which enables making requests to the ``/oauth/token`` endpoint. - -.. code-block:: python - - from auth0.v3.authentication import GetToken - - token = GetToken('myaccount.auth0.com') - - token.login(client_id='...', client_secret='...', username='user@domain.com', password='secr3t', realm='Username-Password-Authentication') - - -ID Token validation -------------------- - -Upon successful authentication, the credentials received may include an ``id_token``, if the authentication request contained the ``openid`` scope. The ``id_token`` contains information associated with the authenticated user. You can read more about ID tokens `here `__. - -Before you access its contents, you must verify that the ID token has not been tampered with and that it is meant for your application to consume. The ``TokenVerifier`` class can be used to perform this verification. - -To create a ``TokenVerifier``, the following arguments are required: - -- A ``SignatureVerifier`` instance, which is responsible for verifying the token's algorithm name and signature. -- The expected issuer value, which typically matches the Auth0 domain prefixed with ``https://`` and suffixed with ``/``. -- The expected audience value, which typically matches the Auth0 application client ID. - -The type of ``SignatureVerifier`` used depends upon the signing algorithm used by your Auth0 application. You can view this value in your application settings under ``Advanced settings | OAuth | JsonWebToken Signature Algorithm``. Auth0 recommends using the RS256 asymmetric signing algorithm. You can read more about signing algorithms `here `__. - -For asymmetric algorithms like RS256, use the ``AsymmetricSignatureVerifier`` class, passing -the public URL where the certificates for the public keys can be found. This will typically be your Auth0 domain with the ``/.well-known/jwks.json`` path appended to it. For example, ``https://your-domain.auth0.com/.well-known/jwks.json``. - -For symmetric algorithms like HS256, use the ``SymmetricSignatureVerifier`` class, passing the value of the client secret of your Auth0 application. - -The following example demonstrates the verification of an ID token signed with the RS256 signing algorithm: - -.. code-block:: python - - from auth0.v3.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier - - domain = 'myaccount.auth0.com' - client_id = 'exampleid' - - # After authenticating - id_token = auth_result['id_token'] - - jwks_url = 'https://{}/.well-known/jwks.json'.format(domain) - issuer = 'https://{}/'.format(domain) - - sv = AsymmetricSignatureVerifier(jwks_url) # Reusable instance - tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id) - tv.verify(id_token) - -If the token verification fails, a ``TokenValidationError`` will be raised. In that scenario, the ID token should be deemed invalid and its contents should not be trusted. - - -Organizations -------------- - -`Organizations `__ is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. - -Using Organizations, you can: -* Represent teams, business customers, partner companies, or any logical grouping of users that should have different ways of accessing your applications, as organizations. -* Manage their membership in a variety of ways, including user invitation. -* Configure branded, federated login flows for each organization. -* Implement role-based access control, such that users can have different roles when authenticating in the context of different organizations. -* Build administration capabilities into your products, using Organizations APIs, so that those businesses can manage their own organizations. - -Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. - - -Log in to an organization -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Log in to an organization by specifying the ``organization`` property when calling ``authorize()``: - -.. code-block:: python - - from auth0.v3.authentication.authorize_client import AuthorizeClient - - client = AuthorizeClient('my.domain.com') - - client.authorize(client_id='client_id', - redirect_uri='http://localhost', - organization="org_abc") - -When logging into an organization, it is important to ensure the ``org_id`` claim of the ID Token matches the expected organization value. The ``TokenVerifier`` can be be used to ensure the ID Token contains the expected ``org_id`` claim value: - -.. code-block:: python - - from auth0.v3.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier - - domain = 'myaccount.auth0.com' - client_id = 'exampleid' - - # After authenticating - id_token = auth_result['id_token'] - - jwks_url = 'https://{}/.well-known/jwks.json'.format(domain) - issuer = 'https://{}/'.format(domain) - - sv = AsymmetricSignatureVerifier(jwks_url) # Reusable instance - tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id) - - # pass the expected organization the user logged in to: - tv.verify(id_token, organization='org_abc') - - -Accept user invitations -^^^^^^^^^^^^^^^^^^^^^^^ - -Accept a user invitation by specifying the ``invitation`` property when calling ``authorize()``. Note that you must also specify the ``organization`` if providing an ``invitation``. -The ID of the invitation and organization are available as query parameters on the invitation URL, e.g., ``https://your-domain.auth0.com/login?invitation=invitation_id&organization=org_id&organization_name=org_name`` - -.. code-block:: python - - from auth0.v3.authentication.authorize_client import AuthorizeClient - - client = AuthorizeClient('my.domain.com') - - client.authorize(client_id='client_id', - redirect_uri='http://localhost', - organization='org_abc', - invitation="invitation_123") - - -Authorizing users from an Organization -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If an ``org_id`` claim is present in the Access Token, then the claim should be validated by the API to ensure that the value received is expected or known. - -In particular: - -- The issuer (``iss``) claim should be checked to ensure the token was issued by Auth0 -- The organization ID (``org_id``) claim should be checked to ensure it is a value that is already known to the application. This could be validated against a known list of organization IDs, or perhaps checked in conjunction with the current request URL. e.g. the sub-domain may hint at what organization should be used to validate the Access Token. - -Normally, validating the issuer would be enough to ensure that the token was issued by Auth0. In the case of organizations, additional checks should be made so that the organization within an Auth0 tenant is expected. - -If the claim cannot be validated, then the application should deem the token invalid. - -The snippet below attempts to illustrate how this verification could look like using the external `PyJWT `__ library. This dependency will take care of pulling the RS256 Public Key that was used by the server to sign the Access Token. It will also validate its signature, expiration, and the audience value. After the basic verification, get the ``org_id`` claim and check it against the expected value. The code assumes your application is configured to sign tokens using the RS256 algorithm. Check the `Validate JSON Web Tokens `__ article to learn more about this verification. - -.. code-block:: python - - import jwt # PyJWT - from jwt import PyJWKClient - - access_token = # access token from the request - url = 'https://{YOUR AUTH0 DOMAIN}/.well-known/jwks.json' - jwks_client = PyJWKClient(url) - signing_key = jwks_client.get_signing_key_from_jwt(access_token) - data = jwt.decode( - access_token, - signing_key.key, - algorithms=['RS256'], - audience='{YOUR API AUDIENCE}' - ) - - organization = # expected organization ID - if data['org_id'] != organization: - raise Exception('Organization (org_id) claim mismatch') - - # if this line is reached, validation is successful - - -************** -Management SDK -************** - -To use the management library you will need to instantiate an Auth0 object with a domain and a `Management API v2 token `__. Please note that these token last 24 hours, so if you need it constantly you should ask for it programmatically using the client credentials grant with a `non interactive client `__ authorized to access the API. For example: - -.. code-block:: python - - from auth0.v3.authentication import GetToken - - domain = 'myaccount.auth0.com' - non_interactive_client_id = 'exampleid' - non_interactive_client_secret = 'examplesecret' - - get_token = GetToken(domain) - token = get_token.client_credentials(non_interactive_client_id, - non_interactive_client_secret, 'https://{}/api/v2/'.format(domain)) - mgmt_api_token = token['access_token'] - - -Then use the token you've obtained as follows: - -.. code-block:: python - - from auth0.v3.management import Auth0 - - domain = 'myaccount.auth0.com' - mgmt_api_token = 'MGMT_API_TOKEN' - - auth0 = Auth0(domain, mgmt_api_token) - -The ``Auth0()`` object is now ready to take orders! -Let's see how we can use this to get all available connections. -(this action requires the token to have the following scope: ``read:connections``) - -.. code-block:: python - - auth0.connections.all() - -Which will yield a list of connections similar to this: - -.. code-block:: python - - [ - { - 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], - 'id': u'con_ErZf9LpXQDE0cNBr', - 'name': u'Amazon-Connection', - 'options': {u'profile': True, u'scope': [u'profile']}, - 'strategy': u'amazon' - }, - { - 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], - 'id': u'con_i8qF5DPiZ3FdadwJ', - 'name': u'Username-Password-Authentication', - 'options': {u'brute_force_protection': True}, - 'strategy': u'auth0' - } - ] - -Modifying an existing connection is equally as easy. Let's change the name -of connection ``'con_ErZf9LpXQDE0cNBr'``. -(The token will need scope: ``update:connections`` to make this one work) - -.. code-block:: python - - auth0.connections.update('con_ErZf9LpXQDE0cNBr', {'name': 'MyNewName'}) - -That's it! Using the ``get`` method of the connections endpoint we can verify -that the rename actually happened. - -.. code-block:: python - - modified_connection = auth0.connections.get('con_ErZf9LpXQDE0cNBr') - -Which returns something like this - -.. code-block:: python - - { - 'enabled_clients': [u'rOsnWgtw23nje2QCDuDJNVpxlsCylSLE'], - 'id': u'con_ErZf9LpXQDE0cNBr', - 'name': u'MyNewName', - 'options': {u'profile': True, u'scope': [u'profile']}, - 'strategy': u'amazon' - } - -Success! - -All endpoints follow a similar structure to ``connections``, and try to follow as -closely as possible the `API documentation `__. - -============== -Error Handling -============== - -When consuming methods from the API clients, the requests could fail for a number of reasons: -- Invalid data sent as part of the request: An ``Auth0Error` is raised with the error code and description. -- Global or Client Rate Limit reached: A ``RateLimitError`` is raised and the time at which the limit -resets is exposed in the ``reset_at`` property. When the header is unset, this value will be ``-1``. -- Network timeouts: Adjustable by passing a ``timeout`` argument to the client. See the `rate limit docs `__ for details. - - -============== -Supported APIs -============== - -************************ -Authentication Endpoints -************************ - -- API Authorization - Authorization Code Grant (``authentication.AuthorizeClient``) -- Database ( ``authentication.Database`` ) -- Delegated ( ``authentication.Delegated`` ) -- Enterprise ( ``authentication.Enterprise`` ) -- API Authorization - Get Token ( ``authentication.GetToken``) -- Passwordless ( ``authentication.Passwordless`` ) -- RevokeToken ( ``authentication.RevokeToken`` ) -- Social ( ``authentication.Social`` ) -- Users ( ``authentication.Users`` ) - - -******************** -Management Endpoints -******************** - -- Actions() (``Auth0().actions``) -- AttackProtection() (``Auth0().attack_protection``) -- Blacklists() ( ``Auth0().blacklists`` ) -- ClientGrants() ( ``Auth0().client_grants`` ) -- Clients() ( ``Auth0().clients`` ) -- Connections() ( ``Auth0().connections`` ) -- CustomDomains() ( ``Auth0().custom_domains`` ) -- DeviceCredentials() ( ``Auth0().device_credentials`` ) -- EmailTemplates() ( ``Auth0().email_templates`` ) -- Emails() ( ``Auth0().emails`` ) -- Grants() ( ``Auth0().grants`` ) -- Guardian() ( ``Auth0().guardian`` ) -- Hooks() ( ``Auth0().hooks`` ) -- Jobs() ( ``Auth0().jobs`` ) -- LogStreams() ( ``Auth0().log_streams`` ) -- Logs() ( ``Auth0().logs`` ) -- Organizations() ( ``Auth0().organizations`` ) -- Prompts() ( ``Auth0().prompts`` ) -- ResourceServers() (``Auth0().resource_servers`` ) -- Roles() ( ``Auth0().roles`` ) -- RulesConfigs() ( ``Auth0().rules_configs`` ) -- Rules() ( ``Auth0().rules`` ) -- Stats() ( ``Auth0().stats`` ) -- Tenants() ( ``Auth0().tenants`` ) -- Tickets() ( ``Auth0().tickets`` ) -- UserBlocks() (``Auth0().user_blocks`` ) -- UsersByEmail() ( ``Auth0().users_by_email`` ) -- Users() ( ``Auth0().users`` ) - -===== -About -===== - -****** -Author -****** - -`Auth0`_ - -********** -Change Log -********** - -Please see `CHANGELOG.md `__. - -*************** -Issue Reporting -*************** - -If you have found a bug or if you have a feature request, please report them at this repository issues section. -Please do not report security vulnerabilities on the public GitHub issue tracker. -The `Responsible Disclosure Program `__ details the procedure for disclosing security issues. - -************** -What is Auth0? -************** - -Auth0 helps you to: - -* Add authentication with `multiple authentication sources `__, - either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, among others**, - or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. -* Add authentication through more traditional `username/password databases `__. -* Add support for `linking different user accounts `__ with the same user. -* Support for generating signed `JSON Web Tokens `__ to call your APIs and **flow the user identity** securely. -* Analytics of how, when and where users are logging in. -* Pull data from other sources and add it to the user profile, through `JavaScript rules `__. - -*************************** -Create a free Auth0 Account -*************************** - -1. Go to `Auth0 `__ and click Sign Up. -2. Use Google, GitHub or Microsoft Account to log in. - -******* -License -******* - -This project is licensed under the MIT license. See the `LICENSE `_ -file for more info. - -.. _Auth0: https://auth0.com - -.. |pypi| image:: https://img.shields.io/pypi/v/auth0-python.svg?style=flat-square&label=latest%20version - :target: https://pypi.org/project/auth0-python/ - :alt: Latest version released on PyPI - -.. |build| image:: https://img.shields.io/circleci/project/github/auth0/auth0-python.svg?style=flat-square&label=circleci - :target: https://circleci.com/gh/auth0/auth0-python - :alt: Build status - -.. |coverage| image:: https://img.shields.io/codecov/c/github/auth0/auth0-python.svg?style=flat-square&label=codecov - :target: https://codecov.io/gh/auth0/auth0-python - :alt: Test coverage - -.. |license| image:: https://img.shields.io/:license-mit-blue.svg?style=flat-square - :target: https://opensource.org/licenses/MIT - :alt: License diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md new file mode 100644 index 00000000..77f4b3b3 --- /dev/null +++ b/V4_MIGRATION_GUIDE.md @@ -0,0 +1,77 @@ +# V4 Migration Guide + +Guide to migrating from `3.x` to `4.x` + +- [Python <3.7 is no longer supported](#python-37-is-no-longer-supported) +- [The `v3` subfolder has been removed](#the-v3-subfolder-has-been-removed) +- [Client ID and client secret are now specified in the constructor for authentication clients](#client-id-and-client-secret-are-now-specified-in-the-constructor-for-authentication-clients) +- [AuthorizeClient and Logout have been removed](#authorizeclient-and-logout-have-been-removed) +- [Methods that call deprecated endpoints have been removed](#methods-that-call-deprecated-endpoints-have-been-removed) + +## Python <3.7 is no longer supported + +Python <=3.6 and Python 2 are EOL and are no longer supported. + +Also note the new Python [Support Policy](https://github.com/auth0/auth0-python#support-policy) + +## The `v3` subfolder has been removed + +Versioning the import paths was not necessary and made major upgrades unnecessarily complex, so this has been removed and all files have been moved up a directory. + +### Before + +```python +from auth0.v3.management import Auth0 + +auth0 = Auth0(domain, mgmt_api_token) +``` + +### After + +```python +from auth0.management import Auth0 + +auth0 = Auth0(domain, mgmt_api_token) +``` + +## Client ID and client secret are now specified in the constructor for authentication clients + +### Before + +```py +from auth0.authentication import GetToken + +get_token = GetToken('my-domain.us.auth0.com') + +get_token.client_credentials('my-client-id', 'my-client-secret', 'my-api') +``` + +### After + +```py +from auth0.authentication import GetToken + +# `client_secret` is optional (you can now use `client_assertion_signing_key` as an alternative) +get_token = GetToken('my-domain.us.auth0.com', 'my-client-id', client_secret='my-client-secret') + +get_token.client_credentials('my-api') +``` + +## AuthorizeClient and Logout have been removed + +The authorize and logout requests need to be done in a user agent, so it didn't make sense to include them in a REST client. + +## Methods that call deprecated endpoints have been removed + +The following methods have been removed: + +### Authentication + +- `database.login` - Use `get_token.login` +- `passwordless.sms_login` - Use `get_token.passwordless_login` +- `users.tokeninfo` - `users.userinfo` + +### Management + +- `users.delete_all_users` - Use `users.delete` +- `jobs.get_results` - Use `jobs.get` diff --git a/auth0/__init__.py b/auth0/__init__.py index 64b38b01..584a20d1 100644 --- a/auth0/__init__.py +++ b/auth0/__init__.py @@ -1 +1,6 @@ -__version__ = '3.21.0' +# This value is updated by `poetry_dynamic_versioning` during build time from the latest git tag +__version__ = "0.0.0" + +from auth0.exceptions import Auth0Error, RateLimitError, TokenValidationError + +__all__ = ("Auth0Error", "RateLimitError", "TokenValidationError") diff --git a/auth0/asyncify.py b/auth0/asyncify.py new file mode 100644 index 00000000..fb884249 --- /dev/null +++ b/auth0/asyncify.py @@ -0,0 +1,115 @@ +import aiohttp + +from auth0.authentication import Users +from auth0.authentication.base import AuthenticationBase +from auth0.rest import RestClientOptions +from auth0.rest_async import AsyncRestClient + + +def _gen_async(client, method): + m = getattr(client, method) + + async def closure(*args, **kwargs): + return await m(*args, **kwargs) + + return closure + + +def asyncify(cls): + methods = [ + func + for func in dir(cls) + if callable(getattr(cls, func)) and not func.startswith("_") + ] + + class UsersAsyncClient(cls): + def __init__( + self, + domain, + telemetry=True, + timeout=5.0, + protocol="https", + ): + super().__init__(domain, telemetry, timeout, protocol) + self.client = AsyncRestClient(None, telemetry=telemetry, timeout=timeout) + + class AsyncManagementClient(cls): + def __init__( + self, + domain, + token, + telemetry=True, + timeout=5.0, + protocol="https", + rest_options=None, + ): + super().__init__(domain, token, telemetry, timeout, protocol, rest_options) + self.client = AsyncRestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + class AsyncAuthenticationClient(cls): + def __init__( + self, + domain, + client_id, + client_secret=None, + client_assertion_signing_key=None, + client_assertion_signing_alg=None, + telemetry=True, + timeout=5.0, + protocol="https", + ): + super().__init__( + domain, + client_id, + client_secret, + client_assertion_signing_key, + client_assertion_signing_alg, + telemetry, + timeout, + protocol, + ) + self.client = AsyncRestClient( + None, + options=RestClientOptions( + telemetry=telemetry, timeout=timeout, retries=0 + ), + ) + + class Wrapper(cls): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if cls == Users: + self._async_client = UsersAsyncClient(*args, **kwargs) + elif AuthenticationBase in cls.__bases__: + self._async_client = AsyncAuthenticationClient(*args, **kwargs) + else: + self._async_client = AsyncManagementClient(*args, **kwargs) + for method in methods: + setattr( + self, + f"{method}_async", + _gen_async(self._async_client, method), + ) + + def set_session(self, session): + """Set Client Session to improve performance by reusing session. + + Args: + session (aiohttp.ClientSession): The client session which should be closed + manually or within context manager. + """ + self._session = session + self._async_client.client.set_session(self._session) + + async def __aenter__(self): + """Automatically create and set session within context manager.""" + self.set_session(aiohttp.ClientSession()) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Automatically close session within context manager.""" + await self._session.close() + + return Wrapper diff --git a/auth0/v3/authentication/__init__.py b/auth0/authentication/__init__.py similarity index 63% rename from auth0/v3/authentication/__init__.py rename to auth0/authentication/__init__.py index d6e361c1..afe4ce3f 100644 --- a/auth0/v3/authentication/__init__.py +++ b/auth0/authentication/__init__.py @@ -1,10 +1,19 @@ -from .authorize_client import AuthorizeClient from .database import Database from .delegated import Delegated from .enterprise import Enterprise from .get_token import GetToken -from .logout import Logout from .passwordless import Passwordless from .revoke_token import RevokeToken from .social import Social from .users import Users + +__all__ = ( + "Database", + "Delegated", + "Enterprise", + "GetToken", + "Passwordless", + "RevokeToken", + "Social", + "Users", +) diff --git a/auth0/authentication/async_token_verifier.py b/auth0/authentication/async_token_verifier.py new file mode 100644 index 00000000..6aff878b --- /dev/null +++ b/auth0/authentication/async_token_verifier.py @@ -0,0 +1,200 @@ +"""Token Verifier module""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .. import TokenValidationError +from ..rest_async import AsyncRestClient +from .token_verifier import AsymmetricSignatureVerifier, JwksFetcher, TokenVerifier + +if TYPE_CHECKING: + from aiohttp import ClientSession + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey + + +class AsyncAsymmetricSignatureVerifier(AsymmetricSignatureVerifier): + """Async verifier for RSA signatures, which rely on public key certificates. + + Args: + jwks_url (str): The url where the JWK set is located. + algorithm (str, optional): The expected signing algorithm. Defaults to "RS256". + """ + + def __init__(self, jwks_url: str, algorithm: str = "RS256") -> None: + super().__init__(jwks_url, algorithm) + self._fetcher = AsyncJwksFetcher(jwks_url) + + def set_session(self, session: ClientSession) -> None: + """Set Client Session to improve performance by reusing session. + + Args: + session (aiohttp.ClientSession): The client session which should be closed + manually or within context manager. + """ + self._fetcher.set_session(session) + + async def _fetch_key(self, key_id=None): + """Request the JWKS. + + Args: + key_id (str): The key's key id.""" + return await self._fetcher.get_key(key_id) + + async def verify_signature(self, token) -> dict[str, Any]: + """Verifies the signature of the given JSON web token. + + Args: + token (str): The JWT to get its signature verified. + + Raises: + TokenValidationError: if the token cannot be decoded, the algorithm is invalid + or the token's signature doesn't match the calculated one. + """ + kid = self._get_kid(token) + secret_or_certificate = await self._fetch_key(key_id=kid) + + return self._decode_jwt(token, secret_or_certificate) + + +class AsyncJwksFetcher(JwksFetcher): + """Class that async fetches and holds a JSON web key set. + This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it. + + Args: + jwks_url (str): The url where the JWK set is located. + cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._async_client = AsyncRestClient(None) + + def set_session(self, session: ClientSession) -> None: + """Set Client Session to improve performance by reusing session. + + Args: + session (aiohttp.ClientSession): The client session which should be closed + manually or within context manager. + """ + self._async_client.set_session(session) + + async def _fetch_jwks(self, force: bool = False) -> dict[str, RSAPublicKey]: + """Attempts to obtain the JWK set from the cache, as long as it's still valid. + When not, it will perform a network request to the jwks_url to obtain a fresh result + and update the cache value with it. + + Args: + force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False. + """ + if force or self._cache_expired(): + self._cache_value = {} + try: + jwks = await self._async_client.get(self._jwks_url) + self._cache_jwks(jwks) + except: # noqa: E722 + return self._cache_value + return self._cache_value + + self._cache_is_fresh = False + return self._cache_value + + async def get_key(self, key_id: str) -> RSAPublicKey: + """Obtains the JWK associated with the given key id. + + Args: + key_id (str): The id of the key to fetch. + + Returns: + the JWK associated with the given key id. + + Raises: + TokenValidationError: when a key with that id cannot be found + """ + keys = await self._fetch_jwks() + + if keys and key_id in keys: + return keys[key_id] + + if not self._cache_is_fresh: + keys = await self._fetch_jwks(force=True) + if keys and key_id in keys: + return keys[key_id] + raise TokenValidationError(f'RSA Public Key with ID "{key_id}" was not found.') + + +class AsyncTokenVerifier(TokenVerifier): + """Class that verifies ID tokens following the steps defined in the OpenID Connect spec. + An OpenID Connect ID token is not meant to be consumed until it's verified. + + Args: + signature_verifier (AsyncAsymmetricSignatureVerifier): The instance that knows how to verify the signature. + issuer (str): The expected issuer claim value. + audience (str): The expected audience claim value. + leeway (int, optional): The clock skew to accept when verifying date related claims in seconds. + Defaults to 60 seconds. + """ + + def __init__( + self, + signature_verifier: AsyncAsymmetricSignatureVerifier, + issuer: str, + audience: str, + leeway: int = 0, + ) -> None: + if not signature_verifier or not isinstance( + signature_verifier, AsyncAsymmetricSignatureVerifier + ): + raise TypeError( + "signature_verifier must be an instance of AsyncAsymmetricSignatureVerifier." + ) + + self.iss = issuer + self.aud = audience + self.leeway = leeway + self._sv = signature_verifier + self._clock = None # legacy testing requirement + + def set_session(self, session: ClientSession) -> None: + """Set Client Session to improve performance by reusing session. + + Args: + session (aiohttp.ClientSession): The client session which should be closed + manually or within context manager. + """ + self._sv.set_session(session) + + async def verify( + self, + token: str, + nonce: str | None = None, + max_age: int | None = None, + organization: str | None = None, + ) -> dict[str, Any]: + """Attempts to verify the given ID token, following the steps defined in the OpenID Connect spec. + + Args: + token (str): The JWT to verify. + nonce (str, optional): The nonce value sent during authentication. + max_age (int, optional): The max_age value sent during authentication. + organization (str, optional): The expected organization ID (org_id) or organization name (org_name) claim value. This should be specified + when logging in to an organization. + + Returns: + the decoded payload from the token + + Raises: + TokenValidationError: when the token cannot be decoded, the token signing algorithm is not the expected one, + the token signature is invalid or the token has a claim missing or with unexpected value. + """ + + # Verify token presence + if not token or not isinstance(token, str): + raise TokenValidationError("ID token is required but missing.") + + # Verify algorithm and signature + payload = await self._sv.verify_signature(token) + + # Verify claims + self._verify_payload(payload, nonce, max_age, organization) + + return payload diff --git a/auth0/authentication/back_channel_login.py b/auth0/authentication/back_channel_login.py new file mode 100644 index 00000000..d1885f79 --- /dev/null +++ b/auth0/authentication/back_channel_login.py @@ -0,0 +1,69 @@ +from typing import Any, Optional, Union, List, Dict + +from .base import AuthenticationBase + +import json + + +class BackChannelLogin(AuthenticationBase): + """Back-Channel Login endpoint""" + + def back_channel_login( + self, + binding_message: str, + login_hint: str, + scope: str, + authorization_details: Optional[Union[str, List[Dict]]] = None, + requested_expiry: Optional[int] = None, + **kwargs + ) -> Any: + """Send a Back-Channel Login. + + Args: + binding_message (str): Human-readable string displayed on both the device calling /bc-authorize and the userโ€™s + authentication device to ensure the user is approves the correct request. + + login_hint (str): JSON string containing user details for authentication in the iss_sub format.Ensure + serialization before passing. + + scope(str): "openid" is a required scope.Multiple scopes are separated + with whitespace. + + authorization_details (str, list of dict, optional): JSON string or a list of dictionaries representing + Rich Authorization Requests (RAR) details to include in the CIBA request. + + requested_expiry (int, optional): Number of seconds the authentication request is valid for. + Auth0 defaults to 300 seconds (5 mins) if not provided. + + **kwargs: Other fields to send along with the request. + + Returns: + auth_req_id, expires_in, interval + """ + + data = { + "client_id": self.client_id, + "binding_message": binding_message, + "login_hint": login_hint, + "scope": scope, + **kwargs, + } + + if authorization_details is not None: + if isinstance(authorization_details, str): + data["authorization_details"] = authorization_details + elif isinstance(authorization_details, list): + data["authorization_details"] = json.dumps(authorization_details) + + if requested_expiry is not None: + if not isinstance(requested_expiry, int) or requested_expiry <= 0: + raise ValueError("requested_expiry must be a positive integer") + data["requested_expiry"] = str(requested_expiry) + + data.update(kwargs) + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/bc-authorize", + data = data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) diff --git a/auth0/authentication/base.py b/auth0/authentication/base.py new file mode 100644 index 00000000..01c79d2e --- /dev/null +++ b/auth0/authentication/base.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from typing import Any + +from auth0.rest import RestClient, RestClientOptions +from auth0.types import RequestData, TimeoutType + +from .client_authentication import add_client_authentication + +UNKNOWN_ERROR = "a0.sdk.internal.unknown" + + +class AuthenticationBase: + """Base authentication object providing simple REST methods. + + Args: + domain (str): The domain of your Auth0 tenant + client_id (str): Your application's client ID + client_secret (str, optional): Your application's client secret + client_assertion_signing_key (str, optional): Private key used to sign the client assertion JWT. + client_assertion_signing_alg (str, optional): Algorithm used to sign the client assertion JWT (defaults to 'RS256'). + telemetry (bool, optional): Enable or disable telemetry (defaults to True) + timeout (float or tuple, optional): Change the requests connect and read timeout. Pass a tuple to specify both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Useful for testing. (defaults to 'https') + """ + + def __init__( + self, + domain: str, + client_id: str, + client_secret: str | None = None, + client_assertion_signing_key: str | None = None, + client_assertion_signing_alg: str | None = None, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + ) -> None: + self.domain = domain + self.client_id = client_id + self.client_secret = client_secret + self.client_assertion_signing_key = client_assertion_signing_key + self.client_assertion_signing_alg = client_assertion_signing_alg + self.protocol = protocol + self.client = RestClient( + None, + options=RestClientOptions(telemetry=telemetry, timeout=timeout, retries=0), + ) + + def _add_client_authentication(self, payload: dict[str, Any]) -> dict[str, Any]: + return add_client_authentication( + payload, + self.domain, + self.client_id, + self.client_secret, + self.client_assertion_signing_key, + self.client_assertion_signing_alg, + ) + + def post( + self, + url: str, + data: RequestData | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + return self.client.post(url, data=data, headers=headers) + + def authenticated_post( + self, + url: str, + data: dict[str, Any], + headers: dict[str, str] | None = None, + ) -> Any: + return self.client.post( + url, data=self._add_client_authentication(data), headers=headers + ) + + def get( + self, + url: str, + params: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + return self.client.get(url, params, headers) diff --git a/auth0/authentication/client_authentication.py b/auth0/authentication/client_authentication.py new file mode 100644 index 00000000..849058f4 --- /dev/null +++ b/auth0/authentication/client_authentication.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import datetime +import uuid +from typing import Any + +import jwt + + +def create_client_assertion_jwt( + domain: str, + client_id: str, + client_assertion_signing_key: str, + client_assertion_signing_alg: str | None, +) -> str: + """Creates a JWT for the client_assertion field. + + Args: + domain (str): The domain of your Auth0 tenant + client_id (str): Your application's client ID + client_assertion_signing_key (str): Private key used to sign the client assertion JWT + client_assertion_signing_alg (str, optional): Algorithm used to sign the client assertion JWT (defaults to 'RS256') + + Returns: + A JWT signed with the `client_assertion_signing_key`. + """ + client_assertion_signing_alg = client_assertion_signing_alg or "RS256" + now = datetime.datetime.utcnow() + return jwt.encode( + { + "iss": client_id, + "sub": client_id, + "aud": f"https://{domain}/", + "iat": now, + "exp": now + datetime.timedelta(seconds=180), + "jti": str(uuid.uuid4()), + }, + client_assertion_signing_key, + client_assertion_signing_alg, + ) + + +def add_client_authentication( + payload: dict[str, Any], + domain: str, + client_id: str, + client_secret: str | None, + client_assertion_signing_key: str | None, + client_assertion_signing_alg: str | None, +) -> dict[str, Any]: + """Adds the client_assertion or client_secret fields to authenticate a payload. + + Args: + payload (dict): The POST payload that needs additional fields to be authenticated. + domain (str): The domain of your Auth0 tenant + client_id (str): Your application's client ID + client_secret (str, optional): Your application's client secret + client_assertion_signing_key (str, optional): Private key used to sign the client assertion JWT + client_assertion_signing_alg (str, optional): Algorithm used to sign the client assertion JWT (defaults to 'RS256') + + Returns: + A copy of the payload with client authentication fields added. + """ + + authenticated_payload = payload.copy() + if client_assertion_signing_key: + authenticated_payload["client_assertion"] = create_client_assertion_jwt( + domain, + client_id, + client_assertion_signing_key, + client_assertion_signing_alg, + ) + authenticated_payload[ + "client_assertion_type" + ] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + elif client_secret: + authenticated_payload["client_secret"] = client_secret + return authenticated_payload diff --git a/auth0/authentication/database.py b/auth0/authentication/database.py new file mode 100644 index 00000000..17f6322b --- /dev/null +++ b/auth0/authentication/database.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import Any + +from .base import AuthenticationBase + + +class Database(AuthenticationBase): + """Database & Active Directory / LDAP Authentication. + + Args: + domain (str): Your auth0 domain (e.g: username.auth0.com) + """ + + def signup( + self, + email: str, + password: str, + connection: str, + username: str | None = None, + user_metadata: dict[str, Any] | None = None, + given_name: str | None = None, + family_name: str | None = None, + name: str | None = None, + nickname: str | None = None, + picture: str | None = None, + ) -> dict[str, Any]: + """Signup using email and password. + + Args: + email (str): The user's email address. + + password (str): The user's desired password. + + connection (str): The name of the database connection where this user should be created. + + username (str, optional): The user's username, if required by the database connection. + + user_metadata (dict, optional): Additional key-value information to store for the user. + Some limitations apply, see: https://auth0.com/docs/metadata#metadata-restrictions + + given_name (str, optional): The user's given name(s). + + family_name (str, optional): The user's family name(s). + + name (str, optional): The user's full name. + + nickname (str, optional): The user's nickname. + + picture (str, optional): A URI pointing to the user's picture. + + + See: https://auth0.com/docs/api/authentication#signup + """ + body: dict[str, Any] = { + "client_id": self.client_id, + "email": email, + "password": password, + "connection": connection, + } + if username: + body.update({"username": username}) + if user_metadata: + body.update({"user_metadata": user_metadata}) + if given_name: + body.update({"given_name": given_name}) + if family_name: + body.update({"family_name": family_name}) + if name: + body.update({"name": name}) + if nickname: + body.update({"nickname": nickname}) + if picture: + body.update({"picture": picture}) + + data: dict[str, Any] = self.post( + f"{self.protocol}://{self.domain}/dbconnections/signup", data=body + ) + return data + + def change_password( + self, + email: str, + connection: str, + password: str | None = None, + organization: str | None = None, + ) -> str: + """Asks to change a password for a given user. + + email (str): The user's email address. + + connection (str): The name of the database connection where this user should be created. + + organization (str, optional): The id of the Organization associated with the user. + """ + body = { + "client_id": self.client_id, + "email": email, + "connection": connection, + } + if organization: + body["organization"] = organization + + data: str = self.post( + f"{self.protocol}://{self.domain}/dbconnections/change_password", + data=body, + ) + return data diff --git a/auth0/authentication/delegated.py b/auth0/authentication/delegated.py new file mode 100644 index 00000000..1266db11 --- /dev/null +++ b/auth0/authentication/delegated.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Any + +from .base import AuthenticationBase + + +class Delegated(AuthenticationBase): + """Delegated authentication endpoints. + + Args: + domain (str): Your auth0 domain (e.g: username.auth0.com) + """ + + def get_token( + self, + target: str, + api_type: str, + grant_type: str, + id_token: str | None = None, + refresh_token: str | None = None, + scope: str = "openid", + ) -> Any: + """Obtain a delegation token.""" + + if id_token and refresh_token: + raise ValueError("Only one of id_token or refresh_token can be None") + + data = { + "client_id": self.client_id, + "grant_type": grant_type, + "target": target, + "scope": scope, + "api_type": api_type, + } + + if id_token: + data.update({"id_token": id_token}) + elif refresh_token: + data.update({"refresh_token": refresh_token}) + else: + raise ValueError("Either id_token or refresh_token must have a value") + + return self.post(f"{self.protocol}://{self.domain}/delegation", data=data) diff --git a/auth0/authentication/enterprise.py b/auth0/authentication/enterprise.py new file mode 100644 index 00000000..518d1001 --- /dev/null +++ b/auth0/authentication/enterprise.py @@ -0,0 +1,28 @@ +from typing import Any + +from .base import AuthenticationBase + + +class Enterprise(AuthenticationBase): + + """Enterprise endpoints. + + Args: + domain (str): Your auth0 domain (e.g: my-domain.us.auth0.com) + """ + + def saml_metadata(self) -> Any: + """Get SAML2.0 Metadata.""" + + return self.get( + url="{}://{}/samlp/metadata/{}".format( + self.protocol, self.domain, self.client_id + ) + ) + + def wsfed_metadata(self) -> Any: + """Returns the WS-Federation Metadata.""" + + url = "{}://{}/wsfed/FederationMetadata/2007-06/FederationMetadata.xml" + + return self.get(url=url.format(self.protocol, self.domain)) diff --git a/auth0/authentication/get_token.py b/auth0/authentication/get_token.py new file mode 100644 index 00000000..a7ab0dc8 --- /dev/null +++ b/auth0/authentication/get_token.py @@ -0,0 +1,322 @@ +from __future__ import annotations + +from typing import Any + +from .base import AuthenticationBase + + +class GetToken(AuthenticationBase): + + """/oauth/token related endpoints + + Args: + domain (str): Your auth0 domain (e.g: username.auth0.com) + """ + + def authorization_code( + self, + code: str, + redirect_uri: str | None, + grant_type: str = "authorization_code", + ) -> Any: + """Authorization code grant + + This is the OAuth 2.0 grant that regular web apps utilize in order + to access an API. Use this endpoint to exchange an Authorization Code + for a Token. + + Args: + code (str): The Authorization Code received from the /authorize Calls + + redirect_uri (str, optional): This is required only if it was set at + the GET /authorize endpoint. The values must match + + grant_type (str): Denotes the flow you're using. For authorization code + use authorization_code + + Returns: + access_token, id_token + """ + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "code": code, + "grant_type": grant_type, + "redirect_uri": redirect_uri, + }, + ) + + def authorization_code_pkce( + self, + code_verifier: str, + code: str, + redirect_uri: str | None, + grant_type: str = "authorization_code", + ) -> Any: + """Authorization code pkce grant + + This is the OAuth 2.0 grant that mobile apps utilize in order to access an API. + Use this endpoint to exchange an Authorization Code for a Token. + + Args: + code_verifier (str): Cryptographically random key that was used to generate + the code_challenge passed to /authorize. + + code (str): The Authorization Code received from the /authorize Calls + + redirect_uri (str, optional): This is required only if it was set at + the GET /authorize endpoint. The values must match + + grant_type (str): Denotes the flow you're using. For authorization code pkce + use authorization_code + + Returns: + access_token, id_token + """ + + return self.post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "code_verifier": code_verifier, + "code": code, + "grant_type": grant_type, + "redirect_uri": redirect_uri, + }, + ) + + def client_credentials( + self, + audience: str, + grant_type: str = "client_credentials", + organization: str | None = None, + ) -> Any: + """Client credentials grant + + This is the OAuth 2.0 grant that server processes utilize in + order to access an API. Use this endpoint to directly request + an access_token by using the Application Credentials (a Client Id and + a Client Secret). + + Args: + audience (str): The unique identifier of the target API you want to access. + + grant_type (str, optional): Denotes the flow you're using. For client credentials use "client_credentials" + + organization (str, optional): Optional Organization name or ID. When included, the access token returned + will include the org_id and org_name claims + + Returns: + access_token + """ + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "audience": audience, + "grant_type": grant_type, + "organization": organization, + }, + ) + + def login( + self, + username: str, + password: str, + scope: str | None = None, + realm: str | None = None, + audience: str | None = None, + grant_type: str = "http://auth0.com/oauth/grant-type/password-realm", + forwarded_for: str | None = None, + ) -> Any: + """Calls /oauth/token endpoint with password-realm grant type + + + This is the OAuth 2.0 grant that highly trusted apps utilize in order + to access an API. In this flow the end-user is asked to fill in credentials + (username/password) typically using an interactive form in the user-agent + (browser). This information is later on sent to the client and Auth0. + It is therefore imperative that the client is absolutely trusted with + this information. + + Args: + username (str): Resource owner's identifier + + password (str): resource owner's Secret + + scope(str, optional): String value of the different scopes the client is asking for. + Multiple scopes are separated with whitespace. + + realm (str, optional): String value of the realm the user belongs. + Set this if you want to add realm support at this grant. + + audience (str, optional): The unique identifier of the target API you want to access. + + grant_type (str, optional): Denotes the flow you're using. For password realm + use http://auth0.com/oauth/grant-type/password-realm + + forwarded_for (str, optional): End-user IP as a string value. Set this if you want + brute-force protection to work in server-side scenarios. + See https://auth0.com/docs/get-started/authentication-and-authorization-flow/avoid-common-issues-with-resource-owner-password-flow-and-attack-protection + + Returns: + access_token, id_token + """ + headers = None + if forwarded_for: + headers = {"auth0-forwarded-for": forwarded_for} + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "username": username, + "password": password, + "realm": realm, + "scope": scope, + "audience": audience, + "grant_type": grant_type, + }, + headers=headers, + ) + + def refresh_token( + self, + refresh_token: str, + scope: str = "", + grant_type: str = "refresh_token", + ) -> Any: + """Calls /oauth/token endpoint with refresh token grant type + + Use this endpoint to refresh an access token, using the refresh token you got during authorization. + + Args: + refresh_token (str): The refresh token returned from the initial token request. + + scope (str): Use this to limit the scopes of the new access token. + Multiple scopes are separated with whitespace. + + grant_type (str): Denotes the flow you're using. For refresh token + use refresh_token + + Returns: + access_token, id_token + """ + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "refresh_token": refresh_token, + "scope": scope, + "grant_type": grant_type, + }, + ) + + def passwordless_login( + self, username: str, otp: str, realm: str, scope: str, audience: str + ) -> Any: + """Calls /oauth/token endpoint with http://auth0.com/oauth/grant-type/passwordless/otp grant type + + Once the verification code was received, login the user using this endpoint with their + phone number/email and verification code. + + Args: + username (str): The user's phone number or email address. + + otp (str): the user's verification code. + + realm (str): use 'sms' or 'email'. + Should be the same as the one used to start the passwordless flow. + + scope(str): String value of the different scopes the client is asking for. + Multiple scopes are separated with whitespace. + + audience (str): The unique identifier of the target API you want to access. + + Returns: + access_token, id_token + """ + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "username": username, + "otp": otp, + "realm": realm, + "scope": scope, + "audience": audience, + "grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp", + }, + ) + + def backchannel_login( + self, auth_req_id: str, grant_type: str = "urn:openid:params:grant-type:ciba", + ) -> Any: + """Calls /oauth/token endpoint with "urn:openid:params:grant-type:ciba" grant type + + Args: + auth_req_id (str): The id received from /bc-authorize + + grant_type (str): Denotes the flow you're using.For Back Channel login + use urn:openid:params:grant-type:ciba + + Returns: + access_token, id_token, refresh_token, token_type, expires_in, scope and authorization_details + """ + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data={ + "client_id": self.client_id, + "auth_req_id": auth_req_id, + "grant_type": grant_type, + }, + ) + + def access_token_for_connection( + self, + subject_token_type: str, + subject_token: str, + requested_token_type: str, + connection: str | None = None, + grant_type: str = "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token", + login_hint: str = None + ) -> Any: + """Calls /oauth/token endpoint with federated-connection-access-token grant type + + Args: + subject_token_type (str): String containing the type of token. + + subject_token (str): String containing the value of subject_token_type. + + requested_token_type (str): String containing the type of requested token. + + connection (str, optional): Denotes the name of a social identity provider configured to your application + + login_hint (str, optional): A hint to the OpenID Provider regarding the end-user for whom authentication is being requested + + Returns: + access_token, scope, issued_token_type, token_type, expires_in + """ + + data = { + "client_id": self.client_id, + "grant_type": grant_type, + "subject_token_type": subject_token_type, + "subject_token": subject_token, + "requested_token_type": requested_token_type, + "connection": connection, + } + + if login_hint: + data["login_hint"] = login_hint + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/token", + data=data, + ) \ No newline at end of file diff --git a/auth0/authentication/passwordless.py b/auth0/authentication/passwordless.py new file mode 100644 index 00000000..dc4ac1af --- /dev/null +++ b/auth0/authentication/passwordless.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import Any + +from .base import AuthenticationBase + + +class Passwordless(AuthenticationBase): + + """Passwordless connections endpoints. + + Args: + domain (str): Your auth0 domain (e.g: my-domain.us.auth0.com) + """ + + def email( + self, email: str, send: str = "link", auth_params: dict[str, str] | None = None + ) -> Any: + """Start flow sending an email. + + Given the user email address, it will send an email with: + + - A link (default, send:"link"). You can then authenticate with + this user opening the link and he will be automatically logged in + to the application. Optionally, you can append/override + parameters to the link (like scope, redirect_uri, protocol, + response_type, etc.) using auth_params dict. + + - A verification code (send:"code"). You can then authenticate with + this user using email as username and code as password. + + Complete the authentication using the get_token.passwordless_login method. + + Args: + email (str): Email address. + + send (str, optional): Can be: 'link' or 'code'. Defaults to 'link'. + + auth_params (dict, optional): Parameters to append or override. + """ + + data: dict[str, Any] = { + "client_id": self.client_id, + "connection": "email", + "email": email, + "send": send, + } + if auth_params: + data.update({"authParams": auth_params}) + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/passwordless/start", data=data + ) + + def sms(self, phone_number: str) -> Any: + """Start flow sending an SMS message. + + Given the user phone number, it will send an SMS with + a verification code. You can then authenticate with + this user using phone number as username and code as password. + + Complete the authentication using the get_token.passwordless_login method. + + Args: + phone_number (str): Phone number. + """ + + data = { + "client_id": self.client_id, + "connection": "sms", + "phone_number": phone_number, + } + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/passwordless/start", data=data + ) diff --git a/auth0/authentication/pushed_authorization_requests.py b/auth0/authentication/pushed_authorization_requests.py new file mode 100644 index 00000000..12c4fc97 --- /dev/null +++ b/auth0/authentication/pushed_authorization_requests.py @@ -0,0 +1,35 @@ +from typing import Any + +from .base import AuthenticationBase + + + +class PushedAuthorizationRequests(AuthenticationBase): + """Pushed Authorization Request (PAR) endpoint""" + + def pushed_authorization_request( + self, response_type: str, redirect_uri: str, **kwargs + ) -> Any: + """Send a Pushed Authorization Request (PAR). + + Args: + response_type (str): Indicates to Auth0 which OAuth 2.0 flow you want to perform. + redirect_uri (str): The URL to which Auth0 will redirect the browser after authorization has been granted + by the user. + **kwargs: Other fields to send along with the PAR. + For RAR requests, authorization_details parameter should be added in a proper format. See:https://datatracker.ietf.org/doc/html/rfc9396 + For JAR requests, requests parameter should be send with the JWT as the value. See: https://datatracker.ietf.org/doc/html/rfc9126#name-the-request-request-paramet + + See: https://www.rfc-editor.org/rfc/rfc9126.html + """ + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/par", + data={ + "client_id":self.client_id, + "client_secret":self.client_secret, + "response_type": response_type, + "redirect_uri": redirect_uri, + **kwargs, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) \ No newline at end of file diff --git a/auth0/authentication/revoke_token.py b/auth0/authentication/revoke_token.py new file mode 100644 index 00000000..29223d45 --- /dev/null +++ b/auth0/authentication/revoke_token.py @@ -0,0 +1,32 @@ +from typing import Any + +from .base import AuthenticationBase + + +class RevokeToken(AuthenticationBase): + """Revoke Refresh Token endpoint + + Args: + domain (str): Your auth0 domain (e.g: my-domain.us.auth0.com) + """ + + def revoke_refresh_token(self, token: str) -> Any: + """Revokes a Refresh Token if it has been compromised + + Each revocation request invalidates not only the specific token, but all other tokens + based on the same authorization grant. This means that all Refresh Tokens that have + been issued for the same user, application, and audience will be revoked. + + Args: + token (str): The Refresh Token you want to revoke + + See: https://auth0.com/docs/api/authentication#refresh-token + """ + body = { + "client_id": self.client_id, + "token": token, + } + + return self.authenticated_post( + f"{self.protocol}://{self.domain}/oauth/revoke", data=body + ) diff --git a/auth0/v3/authentication/social.py b/auth0/authentication/social.py similarity index 63% rename from auth0/v3/authentication/social.py rename to auth0/authentication/social.py index 78df9aa2..dc9b6a3a 100644 --- a/auth0/v3/authentication/social.py +++ b/auth0/authentication/social.py @@ -1,3 +1,5 @@ +from typing import Any + from .base import AuthenticationBase @@ -6,10 +8,10 @@ class Social(AuthenticationBase): """Social provider's endpoints. Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) + domain (str): Your auth0 domain (e.g: my-domain.us.auth0.com) """ - def login(self, client_id, access_token, connection, scope='openid'): + def login(self, access_token: str, connection: str, scope: str = "openid") -> Any: """Login using a social provider's access token Given the social provider's access_token and the connection specified, @@ -18,8 +20,6 @@ def login(self, client_id, access_token, connection, scope='openid'): Facebook, Google, Twitter and Weibo. Args: - client_id (str): application's client id. - access_token (str): social provider's access_token. connection (str): connection type (e.g: 'facebook') @@ -29,11 +29,11 @@ def login(self, client_id, access_token, connection, scope='openid'): """ return self.post( - '{}://{}/oauth/access_token'.format(self.protocol, self.domain), + f"{self.protocol}://{self.domain}/oauth/access_token", data={ - 'client_id': client_id, - 'access_token': access_token, - 'connection': connection, - 'scope': scope, - } + "client_id": self.client_id, + "access_token": access_token, + "connection": connection, + "scope": scope, + }, ) diff --git a/auth0/authentication/token_verifier.py b/auth0/authentication/token_verifier.py new file mode 100644 index 00000000..2468ec80 --- /dev/null +++ b/auth0/authentication/token_verifier.py @@ -0,0 +1,459 @@ +"""Token Verifier module""" +from __future__ import annotations + +import json +import time +from typing import TYPE_CHECKING, Any, ClassVar + +import jwt +import requests + +from auth0.exceptions import TokenValidationError + +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey + + +class SignatureVerifier: + """Abstract class that will verify a given JSON web token's signature + using the key fetched internally given its key id. + + Args: + algorithm (str): The expected signing algorithm (e.g. RS256). + """ + + DISABLE_JWT_CHECKS: ClassVar[dict[str, bool]] = { + "verify_signature": True, + "verify_exp": False, + "verify_nbf": False, + "verify_iat": False, + "verify_aud": False, + "verify_iss": False, + "require_exp": False, + "require_iat": False, + "require_nbf": False, + } + + def __init__(self, algorithm: str) -> None: + if not algorithm or type(algorithm) != str: + raise ValueError("algorithm must be specified.") + self._algorithm = algorithm + + def _fetch_key(self, key_id: str) -> str | RSAPublicKey: + """Obtains the key associated to the given key id. + Must be implemented by subclasses. + + Args: + key_id (str): The id of the key to fetch. + + Returns: + the key to use for verifying a cryptographic signature + """ + raise NotImplementedError + + def _get_kid(self, token: str) -> str | None: + """Gets the key id from the kid claim of the header of the token + + Args: + token (str): The JWT to get the header from. + + Raises: + TokenValidationError: if the token cannot be decoded, the algorithm is invalid + or the token's signature doesn't match the calculated one. + + Returns: + the key id or None + """ + try: + header = jwt.get_unverified_header(token) + except jwt.exceptions.DecodeError: + raise TokenValidationError("token could not be decoded.") + + alg = header.get("alg", None) + if alg != self._algorithm: + raise TokenValidationError( + 'Signature algorithm of "{}" is not supported. Expected the token ' + 'to be signed with "{}"'.format(alg, self._algorithm) + ) + + return header.get("kid", None) + + def _decode_jwt(self, token: str, secret_or_certificate: str) -> dict[str, Any]: + """Verifies and decodes the given JSON web token with the given public key or shared secret. + + Args: + token (str): The JWT to get its signature verified. + secret_or_certificate (str): The public key or shared secret. + + Raises: + TokenValidationError: if the token cannot be decoded, the algorithm is invalid + or the token's signature doesn't match the calculated one. + """ + try: + decoded = jwt.decode( + jwt=token, + key=secret_or_certificate, + algorithms=[self._algorithm], + options=self.DISABLE_JWT_CHECKS, + ) + except jwt.exceptions.InvalidSignatureError: + raise TokenValidationError("Invalid token signature.") + return decoded + + def verify_signature(self, token: str) -> dict[str, Any]: + """Verifies the signature of the given JSON web token. + + Args: + token (str): The JWT to get its signature verified. + + Raises: + TokenValidationError: if the token cannot be decoded, the algorithm is invalid + or the token's signature doesn't match the calculated one. + """ + kid = self._get_kid(token) + if kid is None: + kid = "" + secret_or_certificate = self._fetch_key(key_id=kid) + + return self._decode_jwt(token, secret_or_certificate) # type: ignore[arg-type] + + +class SymmetricSignatureVerifier(SignatureVerifier): + """Verifier for HMAC signatures, which rely on shared secrets. + + Args: + shared_secret (str): The shared secret used to decode the token. + algorithm (str, optional): The expected signing algorithm. Defaults to "HS256". + """ + + def __init__(self, shared_secret: str, algorithm: str = "HS256") -> None: + super().__init__(algorithm) + self._shared_secret = shared_secret + + def _fetch_key(self, key_id: str = "") -> str: + return self._shared_secret + + +class JwksFetcher: + """Class that fetches and holds a JSON web key set. + This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it. + + Args: + jwks_url (str): The url where the JWK set is located. + cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds. + """ + + CACHE_TTL: ClassVar[int] = 600 # 10 min cache lifetime + + def __init__(self, jwks_url: str, cache_ttl: int = CACHE_TTL) -> None: + self._jwks_url = jwks_url + self._init_cache(cache_ttl) + + def _init_cache(self, cache_ttl: int) -> None: + self._cache_value: dict[str, RSAPublicKey] = {} + self._cache_date = 0.0 + self._cache_ttl = cache_ttl + self._cache_is_fresh = False + + def _cache_expired(self) -> bool: + """Checks if the cache is expired + + Returns: + True if it should use the cache. + """ + return self._cache_date + self._cache_ttl < time.time() + + def _cache_jwks(self, jwks: dict[str, Any]) -> None: + """Cache the response of the JWKS request + + Args: + jwks (dict): The JWKS + """ + self._cache_value = self._parse_jwks(jwks) + self._cache_is_fresh = True + self._cache_date = time.time() + + def _fetch_jwks(self, force: bool = False) -> dict[str, RSAPublicKey]: + """Attempts to obtain the JWK set from the cache, as long as it's still valid. + When not, it will perform a network request to the jwks_url to obtain a fresh result + and update the cache value with it. + + Args: + force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False. + """ + if force or self._cache_expired(): + self._cache_value = {} + response = requests.get(self._jwks_url) + if response.ok: + jwks: dict[str, Any] = response.json() + self._cache_jwks(jwks) + return self._cache_value + + self._cache_is_fresh = False + return self._cache_value + + @staticmethod + def _parse_jwks(jwks: dict[str, Any]) -> dict[str, RSAPublicKey]: + """ + Converts a JWK string representation into a binary certificate in PEM format. + """ + keys: dict[str, RSAPublicKey] = {} + + for key in jwks["keys"]: + # noinspection PyUnresolvedReferences + # requirement already includes cryptography -> pyjwt[crypto] + rsa_key: RSAPublicKey = jwt.algorithms.RSAAlgorithm.from_jwk( + json.dumps(key) + ) + keys[key["kid"]] = rsa_key + return keys + + def get_key(self, key_id: str) -> RSAPublicKey: + """Obtains the JWK associated with the given key id. + + Args: + key_id (str): The id of the key to fetch. + + Returns: + the JWK associated with the given key id. + + Raises: + TokenValidationError: when a key with that id cannot be found + """ + keys = self._fetch_jwks() + + if keys and key_id in keys: + return keys[key_id] + + if not self._cache_is_fresh: + keys = self._fetch_jwks(force=True) + if keys and key_id in keys: + return keys[key_id] + raise TokenValidationError(f'RSA Public Key with ID "{key_id}" was not found.') + + +class AsymmetricSignatureVerifier(SignatureVerifier): + """Verifier for RSA signatures, which rely on public key certificates. + + Args: + jwks_url (str): The url where the JWK set is located. + algorithm (str, optional): The expected signing algorithm. Defaults to "RS256". + cache_ttl (int, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds. + """ + + def __init__( + self, + jwks_url: str, + algorithm: str = "RS256", + cache_ttl: int = JwksFetcher.CACHE_TTL, + ) -> None: + super().__init__(algorithm) + self._fetcher = JwksFetcher(jwks_url, cache_ttl) + + def _fetch_key(self, key_id: str) -> RSAPublicKey: + return self._fetcher.get_key(key_id) + + +class TokenVerifier: + """Class that verifies ID tokens following the steps defined in the OpenID Connect spec. + An OpenID Connect ID token is not meant to be consumed until it's verified. + + Args: + signature_verifier (SignatureVerifier): The instance that knows how to verify the signature. + issuer (str): The expected issuer claim value. + audience (str): The expected audience claim value. + leeway (int, optional): The clock skew to accept when verifying date related claims in seconds. + Defaults to 60 seconds. + """ + + def __init__( + self, + signature_verifier: SignatureVerifier, + issuer: str, + audience: str, + leeway: int = 0, + ) -> None: + if not signature_verifier or not isinstance( + signature_verifier, SignatureVerifier + ): + raise TypeError( + "signature_verifier must be an instance of SignatureVerifier." + ) + + self.iss = issuer + self.aud = audience + self.leeway = leeway + self._sv = signature_verifier + self._clock = None # visible for testing + + def verify( + self, + token: str, + nonce: str | None = None, + max_age: int | None = None, + organization: str | None = None, + ) -> dict[str, Any]: + """Attempts to verify the given ID token, following the steps defined in the OpenID Connect spec. + + Args: + token (str): The JWT to verify. + nonce (str, optional): The nonce value sent during authentication. + max_age (int, optional): The max_age value sent during authentication. + organization (str, optional): The expected organization ID (org_id) or organization name (org_name) claim value. This should be specified + when logging in to an organization. + + Returns: + the decoded payload from the token + + Raises: + TokenValidationError: when the token cannot be decoded, the token signing algorithm is not the expected one, + the token signature is invalid or the token has a claim missing or with unexpected value. + """ + + # Verify token presence + if not token or not isinstance(token, str): + raise TokenValidationError("ID token is required but missing.") + + # Verify algorithm and signature + payload = self._sv.verify_signature(token) + + # Verify claims + self._verify_payload(payload, nonce, max_age, organization) + + return payload + + def _verify_payload( + self, + payload: dict[str, Any], + nonce: str | None = None, + max_age: int | None = None, + organization: str | None = None, + ) -> None: + # Issuer + if "iss" not in payload or not isinstance(payload["iss"], str): + raise TokenValidationError( + "Issuer (iss) claim must be a string present in the ID token" + ) + if payload["iss"] != self.iss: + raise TokenValidationError( + 'Issuer (iss) claim mismatch in the ID token; expected "{}", ' + 'found "{}"'.format(self.iss, payload["iss"]) + ) + + # Subject + if "sub" not in payload or not isinstance(payload["sub"], str): + raise TokenValidationError( + "Subject (sub) claim must be a string present in the ID token" + ) + + # Audience + if "aud" not in payload or not isinstance(payload["aud"], (str, list)): + raise TokenValidationError( + "Audience (aud) claim must be a string or array of strings present in" + " the ID token" + ) + + if isinstance(payload["aud"], list) and self.aud not in payload["aud"]: + payload_audiences = ", ".join(payload["aud"]) + raise TokenValidationError( + 'Audience (aud) claim mismatch in the ID token; expected "{}" but was ' + 'not one of "{}"'.format(self.aud, payload_audiences) + ) + elif isinstance(payload["aud"], str) and payload["aud"] != self.aud: + raise TokenValidationError( + 'Audience (aud) claim mismatch in the ID token; expected "{}" ' + 'but found "{}"'.format(self.aud, payload["aud"]) + ) + + # --Time validation (epoch)-- + now = self._clock or time.time() + leeway = self.leeway + + # Expires at + if "exp" not in payload or not isinstance(payload["exp"], int): + raise TokenValidationError( + "Expiration Time (exp) claim must be a number present in the ID token" + ) + + exp_time = payload["exp"] + leeway + if now > exp_time: + raise TokenValidationError( + "Expiration Time (exp) claim error in the ID token; current time ({})" + " is after expiration time ({})".format(now, exp_time) + ) + + # Issued at + if "iat" not in payload or not isinstance(payload["iat"], int): + raise TokenValidationError( + "Issued At (iat) claim must be a number present in the ID token" + ) + + # Nonce + if nonce: + if "nonce" not in payload or not isinstance(payload["nonce"], str): + raise TokenValidationError( + "Nonce (nonce) claim must be a string present in the ID token" + ) + if payload["nonce"] != nonce: + raise TokenValidationError( + 'Nonce (nonce) claim mismatch in the ID token; expected "{}", ' + 'found "{}"'.format(nonce, payload["nonce"]) + ) + + # Organization + if organization: + if organization.startswith("org_"): + if "org_id" not in payload or not isinstance(payload["org_id"], str): + raise TokenValidationError( + "Organization (org_id) claim must be a string present in the ID" + " token" + ) + if payload["org_id"] != organization: + raise TokenValidationError( + "Organization (org_id) claim mismatch in the ID token; expected" + ' "{}", found "{}"'.format(organization, payload["org_id"]) + ) + else: + if "org_name" not in payload or not isinstance( + payload["org_name"], str + ): + raise TokenValidationError( + "Organization (org_name) claim must be a string present in the ID" + " token" + ) + if payload["org_name"] != organization.lower(): + raise TokenValidationError( + "Organization (org_name) claim mismatch in the ID token; expected" + ' "{}", found "{}"'.format(organization, payload["org_name"]) + ) + + # Authorized party + if isinstance(payload["aud"], list) and len(payload["aud"]) > 1: + if "azp" not in payload or not isinstance(payload["azp"], str): + raise TokenValidationError( + "Authorized Party (azp) claim must be a string present in the ID" + " token when Audience (aud) claim has multiple values" + ) + if payload["azp"] != self.aud: + raise TokenValidationError( + "Authorized Party (azp) claim mismatch in the ID token; expected" + ' "{}", found "{}"'.format(self.aud, payload["azp"]) + ) + + # Authentication time + if max_age: + if "auth_time" not in payload or not isinstance(payload["auth_time"], int): + raise TokenValidationError( + "Authentication Time (auth_time) claim must be a number present in" + " the ID token when Max Age (max_age) is specified" + ) + + auth_valid_until = payload["auth_time"] + max_age + leeway + if now > auth_valid_until: + raise TokenValidationError( + "Authentication Time (auth_time) claim in the ID token indicates" + " that too much time has passed since the last end-user" + " authentication. Current time ({}) is after last auth at ({})".format( + now, auth_valid_until + ) + ) diff --git a/auth0/authentication/users.py b/auth0/authentication/users.py new file mode 100644 index 00000000..f0231fdc --- /dev/null +++ b/auth0/authentication/users.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Any + +from auth0.rest import RestClient, RestClientOptions +from auth0.types import TimeoutType + + +class Users: + """Users client. + + Args: + domain (str): The domain of your Auth0 tenant + telemetry (bool, optional): Enable or disable telemetry (defaults to True) + timeout (float or tuple, optional): Change the requests connect and read timeout. Pass a tuple to specify both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Useful for testing. (defaults to 'https') + """ + + def __init__( + self, + domain: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + None, + options=RestClientOptions(telemetry=telemetry, timeout=timeout, retries=0), + ) + + """Userinfo related endpoints. + + Args: + domain (str): Your auth0 domain (e.g: username.auth0.com) + """ + + def userinfo(self, access_token: str) -> dict[str, Any]: + """Returns the user information based on the Auth0 access token. + This endpoint will work only if openid was granted as a scope for the access_token. + + Args: + access_token (str): Auth0 access token (obtained during login). + + Returns: + The user profile. + """ + data: dict[str, Any] = self.client.get( + url=f"{self.protocol}://{self.domain}/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + ) + return data diff --git a/auth0/exceptions.py b/auth0/exceptions.py new file mode 100644 index 00000000..03801e68 --- /dev/null +++ b/auth0/exceptions.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import Any + + +class Auth0Error(Exception): + def __init__( + self, + status_code: int, + error_code: str, + message: str, + content: Any | None = None, + headers: Any | None = None, + ) -> None: + self.status_code = status_code + self.error_code = error_code + self.message = message + self.content = content + self.headers = headers + + def __str__(self) -> str: + return f"{self.status_code}: {self.message}" + + +class RateLimitError(Auth0Error): + def __init__(self, error_code: str, message: str, reset_at: int, headers: Any | None = None) -> None: + super().__init__(status_code=429, error_code=error_code, message=message, headers=headers) + self.reset_at = reset_at + + +class TokenValidationError(Exception): + pass diff --git a/auth0/management/__init__.py b/auth0/management/__init__.py new file mode 100644 index 00000000..761446b6 --- /dev/null +++ b/auth0/management/__init__.py @@ -0,0 +1,72 @@ +from ..utils import is_async_available +from .actions import Actions +from .attack_protection import AttackProtection +from .blacklists import Blacklists +from .branding import Branding +from .client_credentials import ClientCredentials +from .client_grants import ClientGrants +from .clients import Clients +from .connections import Connections +from .custom_domains import CustomDomains +from .device_credentials import DeviceCredentials +from .email_templates import EmailTemplates +from .emails import Emails +from .grants import Grants +from .guardian import Guardian +from .hooks import Hooks +from .jobs import Jobs +from .log_streams import LogStreams +from .logs import Logs +from .network_acls import NetworkAcls +from .organizations import Organizations +from .resource_servers import ResourceServers +from .roles import Roles +from .rules import Rules +from .rules_configs import RulesConfigs +from .self_service_profiles import SelfServiceProfiles +from .stats import Stats +from .tenants import Tenants +from .tickets import Tickets +from .user_blocks import UserBlocks +from .users import Users +from .users_by_email import UsersByEmail + +if is_async_available(): + from .async_auth0 import AsyncAuth0 as Auth0 +else: # pragma: no cover + from .auth0 import Auth0 # type: ignore[assignment] + +__all__ = ( + "Auth0", + "Actions", + "AttackProtection", + "Blacklists", + "Branding", + "ClientCredentials", + "ClientGrants", + "Clients", + "Connections", + "CustomDomains", + "DeviceCredentials", + "EmailTemplates", + "Emails", + "Grants", + "Guardian", + "Hooks", + "Jobs", + "LogStreams", + "Logs", + "NetworkAcls" + "Organizations", + "ResourceServers", + "Roles", + "RulesConfigs", + "Rules", + "SelfServiceProfiles", + "Stats", + "Tenants", + "Tickets", + "UserBlocks", + "UsersByEmail", + "Users", +) diff --git a/auth0/v3/management/actions.py b/auth0/management/actions.py similarity index 58% rename from auth0/v3/management/actions.py rename to auth0/management/actions.py index 8c7e7e6a..bae07f96 100644 --- a/auth0/v3/management/actions.py +++ b/auth0/management/actions.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Actions(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Actions: """Auth0 Actions endpoints Args: @@ -17,25 +22,46 @@ class Actions(object): both values separately or a float to set both to it. (defaults to 5.0 for both) - rest_options (RestClientOptions): Pass an instance of + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions, optional): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, *args): - url = '{}://{}/api/v2/actions'.format(self.protocol, self.domain) + def _url(self, *args: str | None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/actions" for p in args: if p is not None: - url = '{}/{}'.format(url, p) + url = f"{url}/{p}" return url - def get_actions(self, trigger_id=None, action_name=None, deployed=False, installed=False, page=None, per_page=None): + def get_actions( + self, + trigger_id: str | None = None, + action_name: str | None = None, + deployed: bool | None = None, + installed: bool = False, + page: int | None = None, + per_page: int | None = None, + ) -> Any: """Get all actions. Args: @@ -58,18 +84,21 @@ def get_actions(self, trigger_id=None, action_name=None, deployed=False, install See: https://auth0.com/docs/api/management/v2#!/Actions/get_actions """ + + deployed_str = str(deployed).lower() if deployed is not None else None + params = { - 'triggerId': trigger_id, - 'actionName': action_name, - 'deployed': str(deployed).lower(), - 'installed': str(installed).lower(), - 'page': page, - 'per_page': per_page + "triggerId": trigger_id, + "actionName": action_name, + "deployed": deployed_str, + "installed": str(installed).lower(), + "page": page, + "per_page": per_page, } - return self.client.get(self._url('actions'), params=params) + return self.client.get(self._url("actions"), params=params) - def create_action(self, body): + def create_action(self, body: dict[str, Any]) -> dict[str, Any]: """Create a new action. Args: @@ -78,9 +107,9 @@ def create_action(self, body): See: https://auth0.com/docs/api/management/v2#!/Actions/post_action """ - return self.client.post(self._url('actions'), data=body) + return self.client.post(self._url("actions"), data=body) - def update_action(self, id, body): + def update_action(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Updates an action. Args: @@ -91,9 +120,9 @@ def update_action(self, id, body): See: https://auth0.com/docs/api/management/v2#!/Actions/patch_action """ - return self.client.patch(self._url('actions', id), data=body) + return self.client.patch(self._url("actions", id), data=body) - def get_action(self, id): + def get_action(self, id: str) -> dict[str, Any]: """Retrieves an action by its ID. Args: @@ -103,36 +132,34 @@ def get_action(self, id): """ params = {} - return self.client.get(self._url('actions', id), params=params) + return self.client.get(self._url("actions", id), params=params) - def delete_action(self, id, force=False): + def delete_action(self, id: str, force: bool = False) -> Any: """Deletes an action and all of its associated versions. Args: id (str): ID of the action to delete. - force (bool, optional): True to force action deletion detaching bindings, + force (bool, optional): True to force action deletion detaching bindings, False otherwise. Defaults to False. - + See: https://auth0.com/docs/api/management/v2#!/Actions/delete_action """ - params = { - 'force': str(force).lower() - } + params = {"force": str(force).lower()} - return self.client.delete(self._url('actions', id), params=params) - - def get_triggers(self): + return self.client.delete(self._url("actions", id), params=params) + + def get_triggers(self) -> dict[str, Any]: """Retrieve the set of triggers currently available within actions. See: https://auth0.com/docs/api/management/v2#!/Actions/get_triggers """ params = {} - return self.client.get(self._url('triggers'), params=params) + return self.client.get(self._url("triggers"), params=params) - def get_execution(self, id): - """Get information about a specific execution of a trigger. + def get_execution(self, id: str) -> dict[str, Any]: + """Get information about a specific execution of a trigger. Args: id (str): The ID of the execution to retrieve. @@ -141,11 +168,13 @@ def get_execution(self, id): """ params = {} - return self.client.get(self._url('executions', id), params=params) + return self.client.get(self._url("executions", id), params=params) - def get_action_versions(self, id, page=None, per_page=None): + def get_action_versions( + self, id: str, page: int | None = None, per_page: int | None = None + ) -> dict[str, Any]: """Get all of an action's versions. - + Args: id (str): The ID of the action. @@ -157,14 +186,13 @@ def get_action_versions(self, id, page=None, per_page=None): See: https://auth0.com/docs/api/management/v2#!/Actions/get_action_versions """ - params = { - 'page': page, - 'per_page': per_page - } + params = {"page": page, "per_page": per_page} - return self.client.get(self._url('actions', id, 'versions'), params=params) + return self.client.get(self._url("actions", id, "versions"), params=params) - def get_trigger_bindings(self, id, page=None, per_page=None): + def get_trigger_bindings( + self, id: str, page: int | None = None, per_page: int | None = None + ) -> dict[str, Any]: """Get the actions that are bound to a trigger. Args: @@ -178,14 +206,11 @@ def get_trigger_bindings(self, id, page=None, per_page=None): See: https://auth0.com/docs/api/management/v2#!/Actions/get_bindings """ - params = { - 'page': page, - 'per_page': per_page - } - return self.client.get(self._url('triggers', id, 'bindings'), params=params) + params = {"page": page, "per_page": per_page} + return self.client.get(self._url("triggers", id, "bindings"), params=params) - def get_action_version(self, action_id, version_id): - """Retrieve a specific version of an action. + def get_action_version(self, action_id: str, version_id: str) -> dict[str, Any]: + """Retrieve a specific version of an action. Args: action_id (str): The ID of the action. @@ -196,9 +221,11 @@ def get_action_version(self, action_id, version_id): """ params = {} - return self.client.get(self._url('actions', action_id, 'versions', version_id), params=params) + return self.client.get( + self._url("actions", action_id, "versions", version_id), params=params + ) - def deploy_action(self, id): + def deploy_action(self, id: str) -> dict[str, Any]: """Deploy an action. Args: @@ -206,9 +233,11 @@ def deploy_action(self, id): See: https://auth0.com/docs/api/management/v2#!/Actions/post_deploy_action """ - return self.client.post(self._url('actions', id, 'deploy')) + return self.client.post(self._url("actions", id, "deploy")) - def rollback_action_version(self, action_id, version_id): + def rollback_action_version( + self, action_id: str, version_id: str + ) -> dict[str, Any]: """Roll back to a previous version of an action. Args: @@ -218,17 +247,18 @@ def rollback_action_version(self, action_id, version_id): See: https://auth0.com/docs/api/management/v2#!/Actions/post_deploy_draft_version """ - params = {} - return self.client.post(self._url('actions', action_id, 'versions', version_id, 'deploy'), data={}) + return self.client.post( + self._url("actions", action_id, "versions", version_id, "deploy"), data={} + ) - def update_trigger_bindings(self, id, body): + def update_trigger_bindings(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update a trigger's bindings. Args: id (str): The ID of the trigger to update. - body (dict): Attributes for the updated trigger binding. - + body (dict): Attributes for the updated trigger binding. + See: https://auth0.com/docs/api/management/v2#!/Actions/patch_bindings """ - return self.client.patch(self._url('triggers', id, 'bindings'), data=body) \ No newline at end of file + return self.client.patch(self._url("triggers", id, "bindings"), data=body) diff --git a/auth0/management/async_auth0.py b/auth0/management/async_auth0.py new file mode 100644 index 00000000..1b7e5943 --- /dev/null +++ b/auth0/management/async_auth0.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import aiohttp + +from ..asyncify import asyncify +from .auth0 import Auth0 + +if TYPE_CHECKING: + from types import TracebackType + + from auth0.rest import RestClientOptions + + +class AsyncAuth0: + """Provides easy access to all endpoint classes + + Args: + domain (str): Your Auth0 domain, for example 'username.auth0.com' + + token (str): Management API v2 Token + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, domain: str, token: str, rest_options: RestClientOptions | None = None + ) -> None: + self._services = [] + for name, attr in vars(Auth0(domain, token, rest_options=rest_options)).items(): + cls = asyncify(attr.__class__) + service = cls(domain=domain, token=token, rest_options=rest_options) + self._services.append(service) + setattr( + self, + name, + service, + ) + + def set_session(self, session: aiohttp.ClientSession) -> None: + """Set Client Session to improve performance by reusing session. + + Args: + session (aiohttp.ClientSession): The client session which should be closed + manually or within context manager. + """ + self._session = session + for service in self._services: + service.set_session(self._session) + + async def __aenter__(self) -> AsyncAuth0: + """Automatically create and set session within context manager.""" + self.set_session(aiohttp.ClientSession()) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Automatically close session within context manager.""" + await self._session.close() diff --git a/auth0/v3/management/attack_protection.py b/auth0/management/attack_protection.py similarity index 62% rename from auth0/v3/management/attack_protection.py rename to auth0/management/attack_protection.py index 6a0d82c3..0d47cf0b 100644 --- a/auth0/v3/management/attack_protection.py +++ b/auth0/management/attack_protection.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class AttackProtection(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class AttackProtection: """Auth0 attack protection endpoints Args: @@ -17,31 +22,48 @@ class AttackProtection(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, component): - return '{}://{}/api/v2/attack-protection/{}'.format(self.protocol, self.domain, component) + def _url(self, component: str) -> str: + return "{}://{}/api/v2/attack-protection/{}".format( + self.protocol, self.domain, component + ) - def get_breached_password_detection(self): + def get_breached_password_detection(self) -> dict[str, Any]: """Get breached password detection settings. Returns the breached password detection settings. See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_breached_password_detection """ - url = self._url('breached-password-detection') + url = self._url("breached-password-detection") return self.client.get(url) - def update_breached_password_detection(self, body): + def update_breached_password_detection( + self, body: dict[str, Any] + ) -> dict[str, Any]: """Update breached password detection settings. Returns the breached password detection settings. @@ -52,20 +74,20 @@ def update_breached_password_detection(self, body): See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_breached_password_detection """ - url = self._url('breached-password-detection') + url = self._url("breached-password-detection") return self.client.patch(url, data=body) - def get_brute_force_protection(self): + def get_brute_force_protection(self) -> dict[str, Any]: """Get the brute force configuration. Returns the brute force configuration. See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_brute_force_protection """ - url = self._url('brute-force-protection') + url = self._url("brute-force-protection") return self.client.get(url) - def update_brute_force_protection(self, body): + def update_brute_force_protection(self, body: dict[str, Any]) -> dict[str, Any]: """Update the brute force configuration. Returns the brute force configuration. @@ -76,20 +98,20 @@ def update_brute_force_protection(self, body): See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_brute_force_protection """ - url = self._url('brute-force-protection') + url = self._url("brute-force-protection") return self.client.patch(url, data=body) - def get_suspicious_ip_throttling(self): + def get_suspicious_ip_throttling(self) -> dict[str, Any]: """Get the suspicious IP throttling configuration. Returns the suspicious IP throttling configuration. See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_suspicious_ip_throttling """ - url = self._url('suspicious-ip-throttling') + url = self._url("suspicious-ip-throttling") return self.client.get(url) - def update_suspicious_ip_throttling(self, body): + def update_suspicious_ip_throttling(self, body: dict[str, Any]) -> dict[str, Any]: """Update the suspicious IP throttling configuration. Returns the suspicious IP throttling configuration. @@ -100,5 +122,5 @@ def update_suspicious_ip_throttling(self, body): See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_suspicious_ip_throttling """ - url = self._url('suspicious-ip-throttling') + url = self._url("suspicious-ip-throttling") return self.client.patch(url, data=body) diff --git a/auth0/management/auth0.py b/auth0/management/auth0.py new file mode 100644 index 00000000..4edf4c31 --- /dev/null +++ b/auth0/management/auth0.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .actions import Actions +from .attack_protection import AttackProtection +from .blacklists import Blacklists +from .branding import Branding +from .client_credentials import ClientCredentials +from .client_grants import ClientGrants +from .clients import Clients +from .connections import Connections +from .custom_domains import CustomDomains +from .device_credentials import DeviceCredentials +from .email_templates import EmailTemplates +from .emails import Emails +from .grants import Grants +from .guardian import Guardian +from .hooks import Hooks +from .jobs import Jobs +from .log_streams import LogStreams +from .logs import Logs +from .network_acls import NetworkAcls +from .organizations import Organizations +from .prompts import Prompts +from .resource_servers import ResourceServers +from .roles import Roles +from .rules import Rules +from .rules_configs import RulesConfigs +from .self_service_profiles import SelfServiceProfiles +from .stats import Stats +from .tenants import Tenants +from .tickets import Tickets +from .user_blocks import UserBlocks +from .users import Users +from .users_by_email import UsersByEmail + +if TYPE_CHECKING: + from auth0.rest import RestClientOptions + + +class Auth0: + """Provides easy access to all endpoint classes + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, domain: str, token: str, rest_options: RestClientOptions | None = None + ): + self.actions = Actions(domain, token, rest_options=rest_options) + self.attack_protection = AttackProtection( + domain, token, rest_options=rest_options + ) + self.blacklists = Blacklists(domain, token, rest_options=rest_options) + self.branding = Branding(domain, token, rest_options=rest_options) + self.client_credentials = ClientCredentials( + domain, token, rest_options=rest_options + ) + self.client_grants = ClientGrants(domain, token, rest_options=rest_options) + self.clients = Clients(domain, token, rest_options=rest_options) + self.connections = Connections(domain, token, rest_options=rest_options) + self.custom_domains = CustomDomains(domain, token, rest_options=rest_options) + self.device_credentials = DeviceCredentials( + domain, token, rest_options=rest_options + ) + self.email_templates = EmailTemplates(domain, token, rest_options=rest_options) + self.emails = Emails(domain, token, rest_options=rest_options) + self.grants = Grants(domain, token, rest_options=rest_options) + self.guardian = Guardian(domain, token, rest_options=rest_options) + self.hooks = Hooks(domain, token, rest_options=rest_options) + self.jobs = Jobs(domain, token, rest_options=rest_options) + self.log_streams = LogStreams(domain, token, rest_options=rest_options) + self.logs = Logs(domain, token, rest_options=rest_options) + self.network_acls = NetworkAcls(domain, token, rest_options=rest_options) + self.organizations = Organizations(domain, token, rest_options=rest_options) + self.prompts = Prompts(domain, token, rest_options=rest_options) + self.resource_servers = ResourceServers( + domain, token, rest_options=rest_options + ) + self.roles = Roles(domain, token, rest_options=rest_options) + self.rules_configs = RulesConfigs(domain, token, rest_options=rest_options) + self.rules = Rules(domain, token, rest_options=rest_options) + self.self_service_profiles = SelfServiceProfiles( + domain, token, rest_options=rest_options + ) + self.stats = Stats(domain, token, rest_options=rest_options) + self.tenants = Tenants(domain, token, rest_options=rest_options) + self.tickets = Tickets(domain, token, rest_options=rest_options) + self.user_blocks = UserBlocks(domain, token, rest_options=rest_options) + self.users_by_email = UsersByEmail(domain, token, rest_options=rest_options) + self.users = Users(domain, token, rest_options=rest_options) diff --git a/auth0/v3/management/blacklists.py b/auth0/management/blacklists.py similarity index 62% rename from auth0/v3/management/blacklists.py rename to auth0/management/blacklists.py index d0906495..233369a1 100644 --- a/auth0/v3/management/blacklists.py +++ b/auth0/management/blacklists.py @@ -1,7 +1,10 @@ -from .rest import RestClient +from __future__ import annotations +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType -class Blacklists(object): + +class Blacklists: """Auth0 blacklists endpoints Args: @@ -17,17 +20,30 @@ class Blacklists(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): - self.url = '{}://{}/api/v2/blacklists/tokens'.format(protocol, domain) - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) - - def get(self, aud=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.url = f"{protocol}://{domain}/api/v2/blacklists/tokens" + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def get(self, aud: str | None = None) -> list[dict[str, str]]: """Retrieves the jti and aud of all tokens in the blacklist. Args: @@ -38,13 +54,11 @@ def get(self, aud=None): See: https://auth0.com/docs/api/management/v2#!/Blacklists/get_tokens """ - params = { - 'aud': aud - } + params = {"aud": aud} return self.client.get(self.url, params=params) - def create(self, jti, aud=None): + def create(self, jti: str, aud: str | None = None) -> dict[str, str]: """Adds a token to the blacklist. Args: @@ -56,10 +70,10 @@ def create(self, jti, aud=None): See: https://auth0.com/docs/api/management/v2#!/Blacklists/post_tokens """ body = { - 'jti': jti, + "jti": jti, } if aud: - body.update({'aud': aud}) + body.update({"aud": aud}) return self.client.post(self.url, data=body) diff --git a/auth0/management/branding.py b/auth0/management/branding.py new file mode 100644 index 00000000..89cead77 --- /dev/null +++ b/auth0/management/branding.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from typing import Any + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Branding: + """Auth0 Branding endpoints + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, *args: str) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/branding" + for p in args: + if p is not None: + url = f"{url}/{p}" + return url + + def get(self) -> dict[str, Any]: + """Retrieve branding settings. Requires "read:branding" scope. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_branding + """ + + return self.client.get(self._url()) + + def update(self, body: dict[str, Any]) -> dict[str, Any]: + """Update branding settings. Requires "update:branding" scope. + + Args: + body (dict): Attributes for the updated trigger binding. + + See: https://auth0.com/docs/api/management/v2#!/Branding/patch_branding + """ + + return self.client.patch(self._url(), data=body) + + def get_template_universal_login(self) -> dict[str, Any]: + """Get template for New Universal Login Experience. Requires "read:branding" scope. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_universal_login + """ + + return self.client.get(self._url("templates", "universal-login")) + + def delete_template_universal_login(self) -> Any: + """Delete template for New Universal Login Experience. Requires "delete:branding" scope. + + See: https://auth0.com/docs/api/management/v2#!/Branding/delete_universal_login + """ + + return self.client.delete(self._url("templates", "universal-login")) + + def update_template_universal_login(self, body: dict[str, Any]) -> dict[str, Any]: + """Update template for New Universal Login Experience. Requires "update:branding" scope. + + Args: + body (str): Complete HTML content to assign to the template. See linked API documentation for example. + + See: https://auth0.com/docs/api/management/v2#!/Branding/put_universal_login + """ + + return self.client.put( + self._url("templates", "universal-login"), + data={"template": body}, + ) + + def get_default_branding_theme(self) -> dict[str, Any]: + """Retrieve default branding theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_default_branding_theme + """ + + return self.client.get(self._url("themes", "default")) + + def get_branding_theme(self, theme_id: str) -> dict[str, Any]: + """Retrieve branding theme. + + Args: + theme_id (str): The theme_id to retrieve branding theme for. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_branding_theme + """ + + return self.client.get(self._url("themes", theme_id)) + + def delete_branding_theme(self, theme_id: str) -> Any: + """Delete branding theme. + + Args: + theme_id (str): The theme_id to delete branding theme for. + + See: https://auth0.com/docs/api/management/v2#!/Branding/delete_branding_theme + """ + + return self.client.delete(self._url("themes", theme_id)) + + def update_branding_theme( + self, theme_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Update branding theme. + + Args: + theme_id (str): The theme_id to update branding theme for. + body (dict): The attributes to set on the theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/patch_branding_theme + """ + + return self.client.patch(self._url("themes", theme_id), data=body) + + def create_branding_theme(self, body: dict[str, Any]) -> dict[str, Any]: + """Create branding theme. + + Args: + body (dict): The attributes to set on the theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/post_branding_theme + """ + + return self.client.post(self._url("themes"), data=body) diff --git a/auth0/management/client_credentials.py b/auth0/management/client_credentials.py new file mode 100644 index 00000000..0acfc684 --- /dev/null +++ b/auth0/management/client_credentials.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import Any + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class ClientCredentials: + """Auth0 client credentials endpoints. + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, client_id: str, id: str | None = None) -> str: + url = "{}://{}/api/v2/clients/{}/credentials".format( + self.protocol, self.domain, client_id + ) + if id is not None: + return f"{url}/{id}" + return url + + def all(self, client_id: str) -> list[dict[str, Any]]: + """Get a list of credentials associated with a client. + + Args: + client_id (string): The id of a client that owns the credentials. + + See: https://auth0.com/docs/api/management/v2#!/Client_Credentials/get_client_credentials + """ + return self.client.get(self._url(client_id)) + + def get(self, client_id: str, id: str) -> dict[str, Any]: + """Retrieve a specified client credential. + + Args: + client_id (string): The id of a client that owns the credential. + + id (string): The id of the credential. + + See: https://auth0.com/docs/api/management/v2#!/Client_Credentials/get_client_credentials_by_id + """ + return self.client.get(self._url(client_id, id)) + + def create(self, client_id: str, body: dict[str, Any]) -> dict[str, Any]: + """Create a credential on a client. + + Args: + client_id (string): The id of a client to create the credential for. + + See: https://auth0.com/docs/api/management/v2#!/Client_Credentials/post_client_credentials + """ + return self.client.post(self._url(client_id), data=body) + + def delete(self, client_id: str, id: str) -> dict[str, Any]: + """Delete a client's credential. + + Args: + id (str): The id of credential to delete. + + See: https://auth0.com/docs/api/management/v2#!/Client_Credentials/delete_client_credentials_by_id + """ + + return self.client.delete(self._url(client_id, id)) diff --git a/auth0/management/client_grants.py b/auth0/management/client_grants.py new file mode 100644 index 00000000..46b2d9d9 --- /dev/null +++ b/auth0/management/client_grants.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +from typing import Any + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class ClientGrants: + """Auth0 client grants endpoints + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/client-grants" + if id is not None: + return f"{url}/{id}" + return url + + def all( + self, + audience: str | None = None, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + client_id: str | None = None, + allow_any_organization: bool | None = None, + ): + """Retrieves all client grants. + + Args: + audience (str, optional): URL encoded audience of a Resource Server + to filter. + + page (int, optional): The result's page number (zero based). When not set, + the default value is up to the server. + + per_page (int, optional): The amount of entries per page. When not set, + the default value is up to the server. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + + client_id (string, optional): The id of a client to filter. + + allow_any_organization (bool, optional): Optional filter on allow_any_organization. + + See: https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants + """ + + params = { + "audience": audience, + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + "client_id": client_id, + "allow_any_organization": allow_any_organization, + } + + return self.client.get(self._url(), params=params) + + def create(self, body: dict[str, Any]) -> dict[str, Any]: + """Creates a client grant. + + Args: + body (dict): Attributes for the new client grant. + + See: https://auth0.com/docs/api/management/v2#!/Client_Grants/post_client_grants + """ + + return self.client.post(self._url(), data=body) + + def delete(self, id: str) -> Any: + """Deletes a client grant. + + Args: + id (str): Id of client grant to delete. + + See: https://auth0.com/docs/api/management/v2#!/Client_Grants/delete_client_grants_by_id + """ + + return self.client.delete(self._url(id)) + + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: + """Modifies a client grant. + + Args: + id (str): The id of the client grant to modify. + + body (dict): Attributes to update. + + See: https://auth0.com/docs/api/management/v2#!/Client_Grants/patch_client_grants_by_id + """ + + return self.client.patch(self._url(id), data=body) + + def get_organizations( + self, + id: str, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + from_param: str | None = None, + take: int | None = None, + ): + """Get the organizations associated to a client grant. + + Args: + id (str): Id of client grant. + + page (int, optional): The result's page number (zero based). When not set, + the default value is up to the server. + + per_page (int, optional): The amount of entries per page. When not set, + the default value is up to the server. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + + from_param (str, optional): Id to start retrieving entries. You can + limit the amount of entries using the take parameter. + + take (int, optional): The total amount of entries to retrieve when + using the from parameter. When not set, the default value is up to the server. + """ + + params = { + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "from": from_param, + "take": take, + } + + return self.client.get(self._url(f"{id}/organizations"), params=params) diff --git a/auth0/v3/management/clients.py b/auth0/management/clients.py similarity index 68% rename from auth0/v3/management/clients.py rename to auth0/management/clients.py index c1ba9a1f..d7cb6b59 100644 --- a/auth0/v3/management/clients.py +++ b/auth0/management/clients.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Clients(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Clients: """Auth0 applications endpoints Args: @@ -17,24 +22,44 @@ class Clients(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/clients'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/clients" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def all(self, fields=None, include_fields=True, page=None, per_page=None, extra_params=None): + def all( + self, + fields: list[str] | None = None, + include_fields: bool = True, + page: int | None = None, + per_page: int | None = None, + extra_params: dict[str, Any] | None = None, + ) -> list[dict[str, Any]]: """Retrieves a list of all the applications. Important: The client_secret and encryption_key attributes can only be @@ -48,7 +73,7 @@ def all(self, fields=None, include_fields=True, page=None, per_page=None, extra_ include_fields (bool, optional): True if the fields specified are to be included in the result, False otherwise. Defaults to True. - page (int): The result's page number (zero based). When not set, + page (int, optional): The result's page number (zero based). When not set, the default value is up to the server. per_page (int, optional): The amount of entries per page. When not set, @@ -61,14 +86,14 @@ def all(self, fields=None, include_fields=True, page=None, per_page=None, extra_ See: https://auth0.com/docs/api/management/v2#!/Clients/get_clients """ params = extra_params or {} - params['fields'] = fields and ','.join(fields) or None - params['include_fields'] = str(include_fields).lower() - params['page'] = page - params['per_page'] = per_page + params["fields"] = fields and ",".join(fields) or None + params["include_fields"] = str(include_fields).lower() + params["page"] = page + params["per_page"] = per_page return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Create a new application. Args: @@ -79,7 +104,9 @@ def create(self, body): return self.client.post(self._url(), data=body) - def get(self, id, fields=None, include_fields=True): + def get( + self, id: str, fields: list[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Retrieves an application by its id. Important: The client_secret, encryption_key and signing_keys @@ -98,12 +125,14 @@ def get(self, id, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Clients/get_clients_by_id """ - params = {'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower()} + params = { + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + } return self.client.get(self._url(id), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes an application and all its related assets. Args: @@ -114,7 +143,7 @@ def delete(self, id): return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Modifies an application. Important: The client_secret, encryption_key and signing_keys @@ -130,7 +159,7 @@ def update(self, id, body): return self.client.patch(self._url(id), data=body) - def rotate_secret(self, id): + def rotate_secret(self, id: str) -> dict[str, Any]: """Rotate a client secret. The generated secret is NOT base64 encoded. Args: @@ -139,7 +168,7 @@ def rotate_secret(self, id): See: https://auth0.com/docs/api/management/v2#!/Clients/post_rotate_secret """ - data = {'id': id} + data = {"id": id} - url = self._url('%s/rotate-secret' % id) + url = self._url("%s/rotate-secret" % id) return self.client.post(url, data=data) diff --git a/auth0/v3/management/connections.py b/auth0/management/connections.py similarity index 67% rename from auth0/v3/management/connections.py rename to auth0/management/connections.py index 40c9367d..0460d951 100644 --- a/auth0/v3/management/connections.py +++ b/auth0/management/connections.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Connections(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Connections: """Auth0 connection endpoints Args: @@ -17,24 +22,46 @@ class Connections(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/connections'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/connections" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def all(self, strategy=None, fields=None, include_fields=True, page=None, per_page=None, extra_params=None): + def all( + self, + strategy: str | None = None, + fields: list[str] | None = None, + include_fields: bool = True, + page: int | None = None, + per_page: int | None = None, + extra_params: dict[str, Any] | None = None, + name: str | None = None, + ) -> list[dict[str, Any]]: """Retrieves all connections. Args: @@ -58,6 +85,8 @@ def all(self, strategy=None, fields=None, include_fields=True, page=None, per_pa the request. The fields, include_fields, page and per_page values specified as parameters take precedence over the ones defined here. + name (str): Provide the name of the connection to retrieve. + See: https://auth0.com/docs/api/management/v2#!/Connections/get_connections Returns: @@ -65,15 +94,18 @@ def all(self, strategy=None, fields=None, include_fields=True, page=None, per_pa """ params = extra_params or {} - params['strategy'] = strategy or None - params['fields'] = fields and ','.join(fields) or None - params['include_fields'] = str(include_fields).lower() - params['page'] = page - params['per_page'] = per_page + params["strategy"] = strategy or None + params["fields"] = fields and ",".join(fields) or None + params["include_fields"] = str(include_fields).lower() + params["page"] = page + params["per_page"] = per_page + params["name"] = name return self.client.get(self._url(), params=params) - def get(self, id, fields=None, include_fields=True): + def get( + self, id: str, fields: list[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Retrieve connection by id. Args: @@ -92,12 +124,14 @@ def get(self, id, fields=None, include_fields=True): A connection object. """ - params = {'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower()} + params = { + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + } return self.client.get(self._url(id), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes a connection and all its users. Args: @@ -111,7 +145,7 @@ def delete(self, id): return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Modifies a connection. Args: @@ -127,7 +161,7 @@ def update(self, id, body): return self.client.patch(self._url(id), data=body) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new connection. Args: @@ -139,7 +173,7 @@ def create(self, body): return self.client.post(self._url(), data=body) - def delete_user_by_email(self, id, email): + def delete_user_by_email(self, id: str, email: str) -> Any: """Deletes a specified connection user by its email. Args: @@ -152,4 +186,4 @@ def delete_user_by_email(self, id, email): Returns: An empty dict. """ - return self.client.delete(self._url(id) + '/users', params={'email': email}) + return self.client.delete(self._url(id) + "/users", params={"email": email}) diff --git a/auth0/v3/management/custom_domains.py b/auth0/management/custom_domains.py similarity index 63% rename from auth0/v3/management/custom_domains.py rename to auth0/management/custom_domains.py index f681d62b..c0d9e1c0 100644 --- a/auth0/v3/management/custom_domains.py +++ b/auth0/management/custom_domains.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class CustomDomains(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class CustomDomains: """Auth0 custom domains endpoints Args: @@ -17,39 +22,52 @@ class CustomDomains(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/custom-domains'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/custom-domains" if id is not None: - return url + '/' + id + return url + "/" + id return url - def all(self): + def all(self) -> list[dict[str, Any]]: """Retrieves all custom domains. See: https://auth0.com/docs/api/management/v2#!/Custom_Domains/get_custom_domains """ return self.client.get(self._url()) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Retrieves custom domain. See: https://auth0.com/docs/api/management/v2#!/Custom_Domains/get_custom_domains_by_id """ - url = self._url('%s' % (id)) + url = self._url("%s" % (id)) return self.client.get(url) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes a grant. Args: @@ -57,10 +75,10 @@ def delete(self, id): See: https://auth0.com/docs/api/management/v2#!/Custom_Domains/delete_custom_domains_by_id """ - url = self._url('%s' % (id)) + url = self._url("%s" % (id)) return self.client.delete(url) - def create_new(self, body): + def create_new(self, body: dict[str, Any]) -> dict[str, Any]: """Configure a new custom domain. Args: @@ -70,7 +88,7 @@ def create_new(self, body): """ return self.client.post(self._url(), data=body) - def verify(self, id): + def verify(self, id: str) -> dict[str, Any]: """Verify a custom domain. Args: @@ -78,5 +96,5 @@ def verify(self, id): See: https://auth0.com/docs/api/management/v2#!/Custom_Domains/post_verify """ - url = self._url('%s/verify' % (id)) + url = self._url("%s/verify" % (id)) return self.client.post(url) diff --git a/auth0/v3/management/device_credentials.py b/auth0/management/device_credentials.py similarity index 63% rename from auth0/v3/management/device_credentials.py rename to auth0/management/device_credentials.py index 23c2c9dc..e289cf49 100644 --- a/auth0/v3/management/device_credentials.py +++ b/auth0/management/device_credentials.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class DeviceCredentials(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class DeviceCredentials: """Auth0 connection endpoints Args: @@ -17,24 +22,47 @@ class DeviceCredentials(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/device-credentials'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/device-credentials" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def get(self, user_id, client_id, type, fields=None, include_fields=True, page=None, per_page=None, include_totals=False): + def get( + self, + user_id: str, + client_id: str, + type: str, + fields: list[str] | None = None, + include_fields: bool = True, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): """List device credentials. Args: @@ -63,18 +91,18 @@ def get(self, user_id, client_id, type, fields=None, include_fields=True, page=N """ params = { - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower(), - 'user_id': user_id, - 'client_id': client_id, - 'type': type, - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower() + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + "user_id": user_id, + "client_id": client_id, + "type": type, + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), } return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Create a device public key. Args: @@ -85,7 +113,7 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def delete(self, id): + def delete(self, id: str) -> Any: """Delete credential. Args: diff --git a/auth0/v3/management/email_templates.py b/auth0/management/email_templates.py similarity index 69% rename from auth0/v3/management/email_templates.py rename to auth0/management/email_templates.py index 6091b1c0..64ccfc23 100644 --- a/auth0/v3/management/email_templates.py +++ b/auth0/management/email_templates.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class EmailTemplates(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class EmailTemplates: """Auth0 email templates endpoints Args: @@ -17,24 +22,37 @@ class EmailTemplates(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/email-templates'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/email-templates" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Create a new email template. Args: @@ -45,7 +63,7 @@ def create(self, body): return self.client.post(self._url(), data=body) - def get(self, template_name): + def get(self, template_name: str) -> dict[str, Any]: """Retrieves an email template by its name. Args: @@ -59,7 +77,7 @@ def get(self, template_name): return self.client.get(self._url(template_name)) - def update(self, template_name, body): + def update(self, template_name: str, body: dict[str, Any]) -> dict[str, Any]: """Update an existing email template. Args: diff --git a/auth0/v3/management/emails.py b/auth0/management/emails.py similarity index 63% rename from auth0/v3/management/emails.py rename to auth0/management/emails.py index 34813eae..5a833b91 100644 --- a/auth0/v3/management/emails.py +++ b/auth0/management/emails.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Emails(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Emails: """Auth0 email endpoints Args: @@ -17,24 +22,39 @@ class Emails(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/emails/provider'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/emails/provider" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def get(self, fields=None, include_fields=True): + def get( + self, fields: list[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Get the email provider. Args: @@ -47,12 +67,14 @@ def get(self, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Emails/get_provider """ - params = {'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower()} + params = { + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + } return self.client.get(self._url(), params=params) - def config(self, body): + def config(self, body: dict[str, Any]) -> dict[str, Any]: """Configure the email provider. Args: @@ -62,14 +84,14 @@ def config(self, body): """ return self.client.post(self._url(), data=body) - def delete(self): + def delete(self) -> Any: """Delete the email provider. (USE WITH CAUTION) See: https://auth0.com/docs/api/management/v2#!/Emails/delete_provider """ return self.client.delete(self._url()) - def update(self, body): + def update(self, body: dict[str, Any]) -> dict[str, Any]: """Update the email provider. Args: diff --git a/auth0/v3/management/grants.py b/auth0/management/grants.py similarity index 61% rename from auth0/v3/management/grants.py rename to auth0/management/grants.py index 24af4a6c..a95d0def 100644 --- a/auth0/v3/management/grants.py +++ b/auth0/management/grants.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Grants(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Grants: """Auth0 grants endpoints Args: @@ -17,24 +22,43 @@ class Grants(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/grants'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/grants" if id is not None: - return url + '/' + id + return url + "/" + id return url - def all(self, page=None, per_page=None, include_totals=False, extra_params=None): + def all( + self, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + extra_params: dict[str, Any] | None = None, + ): """Retrieves all grants. Args: @@ -54,15 +78,17 @@ def all(self, page=None, per_page=None, include_totals=False, extra_params=None) See: https://auth0.com/docs/api/management/v2#!/Grants/get_grants """ params = extra_params or {} - params.update({ - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower() - }) + params.update( + { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + ) return self.client.get(self._url(), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes a grant. Args: @@ -70,5 +96,5 @@ def delete(self, id): See: https://auth0.com/docs/api/management/v2#!/Grants/delete_grants_by_id """ - url = self._url('%s' % (id)) + url = self._url("%s" % (id)) return self.client.delete(url) diff --git a/auth0/v3/management/guardian.py b/auth0/management/guardian.py similarity index 65% rename from auth0/v3/management/guardian.py rename to auth0/management/guardian.py index a65f2a6a..71c016ab 100644 --- a/auth0/v3/management/guardian.py +++ b/auth0/management/guardian.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Guardian(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Guardian: """Auth0 guardian endpoints Args: @@ -17,33 +22,46 @@ class Guardian(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/guardian'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/guardian" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def all_factors(self): + def all_factors(self) -> list[dict[str, Any]]: """Retrieves all factors. Useful to check factor enablement and trial status. See: https://auth0.com/docs/api/management/v2#!/Guardian/get_factors """ - return self.client.get(self._url('factors')) + return self.client.get(self._url("factors")) - def update_factor(self, name, body): + def update_factor(self, name: str, body: dict[str, Any]) -> dict[str, Any]: """Update Guardian factor. Useful to enable / disable factor. @@ -54,10 +72,10 @@ def update_factor(self, name, body): See: https://auth0.com/docs/api/management/v2#!/Guardian/put_factors_by_name """ - url = self._url('factors/{}'.format(name)) + url = self._url(f"factors/{name}") return self.client.put(url, data=body) - def update_templates(self, body): + def update_templates(self, body: dict[str, Any]) -> dict[str, Any]: """Update enrollment and verification SMS templates. Useful to send custom messages on sms enrollment and verification. @@ -68,9 +86,9 @@ def update_templates(self, body): See: https://auth0.com/docs/api/management/v2#!/Guardian/put_templates """ - return self.client.put(self._url('factors/sms/templates'), data=body) + return self.client.put(self._url("factors/sms/templates"), data=body) - def get_templates(self): + def get_templates(self) -> dict[str, Any]: """Get enrollment and verification templates. Retrieve both templates. Useful to check if a different template than @@ -79,9 +97,9 @@ def get_templates(self): See: https://auth0.com/docs/api/management/v2#!/Guardian/get_templates """ - return self.client.get(self._url('factors/sms/templates')) + return self.client.get(self._url("factors/sms/templates")) - def get_enrollment(self, id): + def get_enrollment(self, id: str) -> dict[str, Any]: """Retrieves an enrollment. Useful to check its type and related metadata. @@ -90,10 +108,10 @@ def get_enrollment(self, id): See: https://auth0.com/docs/api/management/v2#!/Guardian/get_enrollments_by_id """ - url = self._url('enrollments/{}'.format(id)) + url = self._url(f"enrollments/{id}") return self.client.get(url) - def delete_enrollment(self, id): + def delete_enrollment(self, id: str) -> Any: """Deletes an enrollment. Useful when you want to force re-enroll. @@ -103,10 +121,10 @@ def delete_enrollment(self, id): See: https://auth0.com/docs/api/management/v2#!/Guardian/delete_enrollments_by_id """ - url = self._url('enrollments/{}'.format(id)) + url = self._url(f"enrollments/{id}") return self.client.delete(url) - def create_enrollment_ticket(self, body): + def create_enrollment_ticket(self, body: dict[str, Any]) -> dict[str, Any]: """Creates an enrollment ticket for user_id A useful way to send an email to a user, with a link that lead to @@ -117,9 +135,9 @@ def create_enrollment_ticket(self, body): See: https://auth0.com/docs/api/management/v2#!/Guardian/post_ticket """ - return self.client.post(self._url('enrollments/ticket'), data=body) + return self.client.post(self._url("enrollments/ticket"), data=body) - def get_factor_providers(self, factor_name, name): + def get_factor_providers(self, factor_name: str, name: str) -> dict[str, Any]: """Get Guardian SNS or SMS factor providers. Returns provider configuration. @@ -132,10 +150,12 @@ def get_factor_providers(self, factor_name, name): See: https://auth0.com/docs/api/management/v2#!/Guardian/get_sns https://auth0.com/docs/api/management/v2#!/Guardian/get_twilio """ - url = self._url('factors/{}/providers/{}'.format(factor_name, name)) + url = self._url(f"factors/{factor_name}/providers/{name}") return self.client.get(url) - def update_factor_providers(self, factor_name, name, body): + def update_factor_providers( + self, factor_name: str, name: str, body: dict[str, Any] + ) -> dict[str, Any]: """Get Guardian factor providers. Returns provider configuration. @@ -149,5 +169,5 @@ def update_factor_providers(self, factor_name, name, body): See: https://auth0.com/docs/api/management/v2#!/Guardian/put_twilio """ - url = self._url('factors/{}/providers/{}'.format(factor_name, name)) + url = self._url(f"factors/{factor_name}/providers/{name}") return self.client.put(url, data=body) diff --git a/auth0/v3/management/hooks.py b/auth0/management/hooks.py similarity index 76% rename from auth0/v3/management/hooks.py rename to auth0/management/hooks.py index 2d8c4c05..3c03aa5b 100644 --- a/auth0/v3/management/hooks.py +++ b/auth0/management/hooks.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Hooks(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Hooks: """Hooks endpoint implementation. @@ -18,31 +23,44 @@ class Hooks(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = "{}://{}/api/v2/hooks".format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/hooks" if id is not None: - return "{}/{}".format(url, id) + return f"{url}/{id}" return url def all( self, - enabled=True, - fields=None, - include_fields=True, - page=None, - per_page=None, - include_totals=False, + enabled: bool = True, + fields: list[str] | None = None, + include_fields: bool = True, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, ): """Retrieves a list of all hooks. @@ -82,7 +100,7 @@ def all( return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new Hook. Args: @@ -91,7 +109,7 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def get(self, id, fields=None): + def get(self, id: str, fields: list[str] | None = None) -> dict[str, Any]: """Retrieves a hook by its ID. Args: @@ -108,7 +126,7 @@ def get(self, id, fields=None): } return self.client.get(self._url(id), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes a hook. Args: @@ -118,7 +136,7 @@ def delete(self, id): """ return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Updates an existing hook. Args: @@ -130,7 +148,7 @@ def update(self, id, body): """ return self.client.patch(self._url(id), data=body) - def get_secrets(self, id): + def get_secrets(self, id: str) -> dict[str, Any]: """Retrieves a hook's secrets. Args: @@ -141,7 +159,7 @@ def get_secrets(self, id): return self.client.get(self._url("%s/secrets" % id)) - def add_secrets(self, id, body): + def add_secrets(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Add one or more secrets for an existing hook. Args: @@ -153,7 +171,7 @@ def add_secrets(self, id, body): """ return self.client.post(self._url("%s/secrets" % id), data=body) - def delete_secrets(self, id, body): + def delete_secrets(self, id: str, body: list[str]) -> Any: """Delete one or more existing secrets for an existing hook. Args: @@ -165,7 +183,7 @@ def delete_secrets(self, id, body): """ return self.client.delete(self._url("%s/secrets" % id), data=body) - def update_secrets(self, id, body): + def update_secrets(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update one or more existing secrets for an existing hook. Args: diff --git a/auth0/v3/management/jobs.py b/auth0/management/jobs.py similarity index 63% rename from auth0/v3/management/jobs.py rename to auth0/management/jobs.py index bc4d5b41..50f8975e 100644 --- a/auth0/v3/management/jobs.py +++ b/auth0/management/jobs.py @@ -1,8 +1,12 @@ -from .rest import RestClient -import warnings +from __future__ import annotations +from typing import Any -class Jobs(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Jobs: """Auth0 jobs endpoints Args: @@ -18,24 +22,37 @@ class Jobs(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, path=None): - url = '{}://{}/api/v2/jobs'.format(self.protocol, self.domain) + def _url(self, path: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/jobs" if path is not None: - return '{}/{}'.format(url, path) + return f"{url}/{path}" return url - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Retrieves a job. Useful to check its status. Args: @@ -45,7 +62,7 @@ def get(self, id): """ return self.client.get(self._url(id)) - def get_failed_job(self, id): + def get_failed_job(self, id: str) -> dict[str, Any]: """Get failed job error details. Args: @@ -53,25 +70,10 @@ def get_failed_job(self, id): See: https://auth0.com/docs/api/management/v2#!/Jobs/get_errors """ - url = self._url('{}/errors'.format(id)) + url = self._url(f"{id}/errors") return self.client.get(url) - def get_results(self, job_id): - """Get results of a job - - Args: - job_id (str): The id of the job. - - Deprecation: - The /jobs/{id}/results endpoint was removed from the Management API. - You can obtain the Job results by querying a Job by ID. - - See: https://auth0.com/docs/api/management/v2#!/Jobs/get_jobs_by_id - """ - warnings.warn("/jobs/{id}/results is no longer available. The get(id) function will be called instead.", DeprecationWarning) - return self.get(job_id) - - def export_users(self, body): + def export_users(self, body: dict[str, Any]): """Export all users to a file using a long running job. Check job status with get(). URL pointing to the export file will be @@ -82,9 +84,16 @@ def export_users(self, body): See: https://auth0.com/docs/api/management/v2#!/Jobs/post_users_exports """ - return self.client.post(self._url('users-exports'), data=body) - - def import_users(self, connection_id, file_obj, upsert=False, send_completion_email=True, external_id=None): + return self.client.post(self._url("users-exports"), data=body) + + def import_users( + self, + connection_id: str, + file_obj: Any, + upsert: bool = False, + send_completion_email: bool = True, + external_id: str | None = None, + ) -> dict[str, Any]: """Imports users to a connection from a file. Args: @@ -107,14 +116,18 @@ def import_users(self, connection_id, file_obj, upsert=False, send_completion_em See: https://auth0.com/docs/api/management/v2#!/Jobs/post_users_imports """ - return self.client.file_post(self._url('users-imports'), - data={'connection_id': connection_id, - 'upsert': str(upsert).lower(), - 'send_completion_email': str(send_completion_email).lower(), - 'external_id': external_id}, - files={'users': file_obj}) - - def send_verification_email(self, body): + return self.client.file_post( + self._url("users-imports"), + data={ + "connection_id": connection_id, + "upsert": str(upsert).lower(), + "send_completion_email": str(send_completion_email).lower(), + "external_id": external_id, + }, + files={"users": file_obj}, + ) + + def send_verification_email(self, body: dict[str, Any]) -> dict[str, Any]: """Send verification email. Send an email to the specified user that asks them to click a link to @@ -125,4 +138,4 @@ def send_verification_email(self, body): See: https://auth0.com/docs/api/v2#!/Jobs/post_verification_email """ - return self.client.post(self._url('verification-email'), data=body) + return self.client.post(self._url("verification-email"), data=body) diff --git a/auth0/v3/management/log_streams.py b/auth0/management/log_streams.py similarity index 67% rename from auth0/v3/management/log_streams.py rename to auth0/management/log_streams.py index 86e9dd7c..62a7b7e7 100644 --- a/auth0/v3/management/log_streams.py +++ b/auth0/management/log_streams.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class LogStreams(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class LogStreams: """Auth0 log streams endpoints Args: @@ -17,24 +22,37 @@ class LogStreams(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/log-streams'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/log-streams" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def list(self): + def list(self) -> list[dict[str, Any]]: """Search log events. Args: @@ -43,7 +61,7 @@ def list(self): return self.client.get(self._url()) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Retrieves the data related to the log stream entry identified by id. Args: @@ -54,7 +72,7 @@ def get(self, id): return self.client.get(self._url(id)) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new log stream. Args: @@ -64,7 +82,7 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def delete(self, id): + def delete(self, id: str) -> dict[str, Any]: """Delete a log stream. Args: @@ -74,7 +92,7 @@ def delete(self, id): """ return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update a log stream with the attributes passed in 'body' Args: diff --git a/auth0/v3/management/logs.py b/auth0/management/logs.py similarity index 64% rename from auth0/v3/management/logs.py rename to auth0/management/logs.py index aa8b646f..54164652 100644 --- a/auth0/v3/management/logs.py +++ b/auth0/management/logs.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Logs(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Logs: """Auth0 logs endpoints Args: @@ -17,26 +22,48 @@ class Logs(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/logs'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/logs" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def search(self, page=0, per_page=50, sort=None, q=None, - include_totals=True, fields=None, from_param=None, take=None, - include_fields=True): + def search( + self, + page: int = 0, + per_page: int = 50, + sort: str | None = None, + q: str | None = None, + include_totals: bool = True, + fields: list[str] | None = None, + from_param: str | None = None, + take: int | None = None, + include_fields: bool = True, + ): """Search log events. Args: @@ -71,19 +98,19 @@ def search(self, page=0, per_page=50, sort=None, q=None, See: https://auth0.com/docs/api/management/v2#!/Logs/get_logs """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower(), - 'sort': sort, - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower(), - 'q': q, - 'from': from_param, - 'take': take + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "sort": sort, + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + "q": q, + "from": from_param, + "take": take, } return self.client.get(self._url(), params=params) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Retrieves the data related to the log entry identified by id. Args: diff --git a/auth0/management/network_acls.py b/auth0/management/network_acls.py new file mode 100644 index 00000000..ccc74589 --- /dev/null +++ b/auth0/management/network_acls.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from typing import Any, List # List is being used as list is already a method. + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class NetworkAcls: + """Auth0 Netwrok Acls endpoints + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/network-acls" + if id is not None: + return f"{url}/{id}" + return url + + def all( + self, + page: int = 0, + per_page: int = 25, + include_totals: bool = True, + ) -> List[dict[str, Any]]: + """List self-service profiles. + + Args: + page (int, optional): The result's page number (zero based). By default, + retrieves the first page of results. + + per_page (int, optional): The amount of entries per page. By default, + retrieves 25 results per page. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to True. + + See: https://auth0.com/docs/api/management/v2/network-acls/get-network-acls + """ + + params = { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(), params=params) + + def create(self, body: dict[str, Any]) -> dict[str, Any]: + """Create a new self-service profile. + + Args: + body (dict): Attributes for the new access control list. + + See: https://auth0.com/docs/api/management/v2/network-acls/post-network-acls + """ + + return self.client.post(self._url(), data=body) + + def get(self, id: str) -> dict[str, Any]: + """Get a self-service profile. + + Args: + id (str): The id of the access control list to retrieve. + + See: https://auth0.com/docs/api/management/v2/network-acls/get-network-acls-by-id + """ + + return self.client.get(self._url(id)) + + def delete(self, id: str) -> None: + """Delete a self-service profile. + + Args: + id (str): The id of the access control list to delete. + + See: https://auth0.com/docs/api/management/v2/network-acls/delete-network-acls-by-id + """ + + self.client.delete(self._url(id)) + + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: + """Update a access control list. + + Args: + id (str): The id of the access control list to update. + + body (dict): Attributes of the access control list to modify. + + See: https://auth0.com/docs/api/management/v2/network-acls/put-network-acls-by-id + """ + + return self.client.put(self._url(id), data=body) + + def update_partial(self, id: str, body: dict[str, Any]) -> dict[str, Any]: + """Update partially the access control list. + + See: https://auth0.com/docs/api/management/v2/network-acls/patch-network-acls-by-id + """ + + return self.client.patch(self._url(id), data=body) + + \ No newline at end of file diff --git a/auth0/v3/management/organizations.py b/auth0/management/organizations.py similarity index 52% rename from auth0/v3/management/organizations.py rename to auth0/management/organizations.py index 19d38da6..8da4f4c0 100644 --- a/auth0/v3/management/organizations.py +++ b/auth0/management/organizations.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Organizations(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Organizations: """Auth0 organizations endpoints Args: @@ -17,26 +22,46 @@ class Organizations(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, *args): - url = '{}://{}/api/v2/organizations'.format(self.protocol, self.domain) + def _url(self, *args: str | None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/organizations" for p in args: if p is not None: - url = '{}/{}'.format(url, p) + url = f"{url}/{p}" return url # Organizations - def all_organizations(self, page=None, per_page=None, include_totals=True, from_param=None, take=None): + def all_organizations( + self, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = True, + from_param: str | None = None, + take: int | None = None, + ): """Retrieves a list of all the organizations. Args: @@ -59,16 +84,16 @@ def all_organizations(self, page=None, per_page=None, include_totals=True, from_ """ params = { - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower(), - 'from': from_param, - 'take': take + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + "from": from_param, + "take": take, } return self.client.get(self._url(), params=params) - def get_organization_by_name(self, name=None): + def get_organization_by_name(self, name: str | None = None) -> dict[str, Any]: """Retrieves an organization given its name. Args: @@ -78,9 +103,9 @@ def get_organization_by_name(self, name=None): """ params = {} - return self.client.get(self._url('name', name), params=params) + return self.client.get(self._url("name", name), params=params) - def get_organization(self, id): + def get_organization(self, id: str) -> dict[str, Any]: """Retrieves an organization by its ID. Args: @@ -92,7 +117,7 @@ def get_organization(self, id): return self.client.get(self._url(id), params=params) - def create_organization(self, body): + def create_organization(self, body: dict[str, Any]) -> dict[str, Any]: """Create a new organization. Args: @@ -103,7 +128,7 @@ def create_organization(self, body): return self.client.post(self._url(), data=body) - def update_organization(self, id, body): + def update_organization(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Modifies an organization. Args: @@ -116,7 +141,7 @@ def update_organization(self, id, body): return self.client.patch(self._url(id), data=body) - def delete_organization(self, id): + def delete_organization(self, id: str) -> Any: """Deletes an organization and all its related assets. Args: @@ -127,9 +152,10 @@ def delete_organization(self, id): return self.client.delete(self._url(id)) - # Organization Connections - def all_organization_connections(self, id, page=None, per_page=None): + def all_organization_connections( + self, id: str, page: int | None = None, per_page: int | None = None + ) -> list[dict[str, Any]]: """Retrieves a list of all the organization connections. Args: @@ -143,10 +169,12 @@ def all_organization_connections(self, id, page=None, per_page=None): See: https://auth0.com/docs/api/management/v2#!/Organizations/get_enabled_connections """ - params = {'page': page, 'per_page': per_page} - return self.client.get(self._url(id, 'enabled_connections'), params=params) + params = {"page": page, "per_page": per_page} + return self.client.get(self._url(id, "enabled_connections"), params=params) - def get_organization_connection(self, id, connection_id): + def get_organization_connection( + self, id: str, connection_id: str + ) -> dict[str, Any]: """Retrieves an organization connection by its ID. Args: @@ -158,9 +186,13 @@ def get_organization_connection(self, id, connection_id): """ params = {} - return self.client.get(self._url(id, 'enabled_connections', connection_id), params=params) + return self.client.get( + self._url(id, "enabled_connections", connection_id), params=params + ) - def create_organization_connection(self, id, body): + def create_organization_connection( + self, id: str, body: dict[str, Any] + ) -> dict[str, Any]: """Adds a connection to an organization. Args: @@ -171,9 +203,11 @@ def create_organization_connection(self, id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/post_enabled_connections """ - return self.client.post(self._url(id, 'enabled_connections'), data=body) + return self.client.post(self._url(id, "enabled_connections"), data=body) - def update_organization_connection(self, id, connection_id, body): + def update_organization_connection( + self, id: str, connection_id: str, body: dict[str, Any] + ) -> dict[str, Any]: """Modifies an organization. Args: @@ -186,9 +220,11 @@ def update_organization_connection(self, id, connection_id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/patch_enabled_connections_by_connectionId """ - return self.client.patch(self._url(id, 'enabled_connections', connection_id), data=body) + return self.client.patch( + self._url(id, "enabled_connections", connection_id), data=body + ) - def delete_organization_connection(self, id, connection_id): + def delete_organization_connection(self, id: str, connection_id: str) -> Any: """Deletes a connection from the given organization. Args: @@ -199,12 +235,25 @@ def delete_organization_connection(self, id, connection_id): See: https://auth0.com/docs/api/management/v2#!/Organizations/delete_enabled_connections_by_connectionId """ - return self.client.delete(self._url(id, 'enabled_connections', connection_id)) + return self.client.delete(self._url(id, "enabled_connections", connection_id)) # Organization Members - def all_organization_members(self, id, page=None, per_page=None, include_totals=True, from_param=None, take=None): + def all_organization_members( + self, + id: str, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = True, + from_param: str | None = None, + take: int | None = None, + fields: list[str] | None = None, + include_fields: bool = True, + ): """Retrieves a list of all the organization members. + Member roles are not sent by default. Use `fields=roles` to retrieve the roles assigned to each listed member. + To use this parameter, you must include the `read:organization_member_roles scope` in the token. + Args: id (str): the ID of the organization. @@ -223,20 +272,31 @@ def all_organization_members(self, id, page=None, per_page=None, include_totals= take (int, optional): The total amount of entries to retrieve when using the from parameter. When not set, the default value is up to the server. - See: https://auth0.com/docs/api/management/v2#!/Organizations/get_members + fields (list of str, optional): A list of fields to include or + exclude from the result (depending on include_fields). If fields is left blank, + all fields (except roles) are returned. + + include_fields (bool, optional): True if the fields specified are + to be included in the result, False otherwise. Defaults to True. + + See: https://auth0.com/docs/api/management/v2/organizations/get-members """ params = { - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower(), - 'from': from_param, - 'take': take + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + "from": from_param, + "take": take, + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), } - return self.client.get(self._url(id, 'members'), params=params) + return self.client.get(self._url(id, "members"), params=params) - def create_organization_members(self, id, body): + def create_organization_members( + self, id: str, body: dict[str, Any] + ) -> dict[str, Any]: """Adds members to an organization. Args: @@ -247,9 +307,9 @@ def create_organization_members(self, id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/post_members """ - return self.client.post(self._url(id, 'members'), data=body) + return self.client.post(self._url(id, "members"), data=body) - def delete_organization_members(self, id, body): + def delete_organization_members(self, id: str, body: dict[str, Any]) -> Any: """Deletes members from the given organization. Args: @@ -260,10 +320,17 @@ def delete_organization_members(self, id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/delete_members """ - return self.client.delete(self._url(id, 'members'), data=body) + return self.client.delete(self._url(id, "members"), data=body) # Organization Member Roles - def all_organization_member_roles(self, id, user_id, page=None, per_page=None): + def all_organization_member_roles( + self, + id: str, + user_id: str, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ) -> list[dict[str, Any]]: """Retrieves a list of all the roles from the given organization member. Args: @@ -277,12 +344,23 @@ def all_organization_member_roles(self, id, user_id, page=None, per_page=None): per_page (int, optional): The amount of entries per page. When not set, the default value is up to the server. + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + See: https://auth0.com/docs/api/management/v2#!/Organizations/get_organization_member_roles """ - params = {'page': page, 'per_page': per_page} - return self.client.get(self._url(id, 'members', user_id, 'roles'), params=params) + params = { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower() + } + return self.client.get( + self._url(id, "members", user_id, "roles"), params=params + ) - def create_organization_member_roles(self, id, user_id, body): + def create_organization_member_roles( + self, id: str, user_id: str, body: dict[str, Any] + ) -> dict[str, Any]: """Adds roles to a member of an organization. Args: @@ -295,9 +373,11 @@ def create_organization_member_roles(self, id, user_id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/post_organization_member_roles """ - return self.client.post(self._url(id, 'members', user_id, 'roles'), data=body) + return self.client.post(self._url(id, "members", user_id, "roles"), data=body) - def delete_organization_member_roles(self, id, user_id, body): + def delete_organization_member_roles( + self, id: str, user_id: str, body: dict[str, Any] + ) -> Any: """Deletes roles from a member of an organization. Args: @@ -310,11 +390,16 @@ def delete_organization_member_roles(self, id, user_id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/delete_organization_member_roles """ - return self.client.delete(self._url(id, 'members', user_id, 'roles'), data=body) - + return self.client.delete(self._url(id, "members", user_id, "roles"), data=body) # Organization Invitations - def all_organization_invitations(self, id, page=None, per_page=None): + def all_organization_invitations( + self, + id: str, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): """Retrieves a list of all the organization invitations. Args: @@ -326,12 +411,21 @@ def all_organization_invitations(self, id, page=None, per_page=None): per_page (int, optional): The amount of entries per page. When not set, the default value is up to the server. + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + NOTE: returns start and limit, total count is not yet supported + See: https://auth0.com/docs/api/management/v2#!/Organizations/get_invitations """ - params = {'page': page, 'per_page': per_page} - return self.client.get(self._url(id, 'invitations'), params=params) + params = { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(id, "invitations"), params=params) - def get_organization_invitation(self, id, invitaton_id): + def get_organization_invitation(self, id: str, invitaton_id: str) -> dict[str, Any]: """Retrieves an organization invitation by its ID. Args: @@ -343,9 +437,13 @@ def get_organization_invitation(self, id, invitaton_id): """ params = {} - return self.client.get(self._url(id, 'invitations', invitaton_id), params=params) + return self.client.get( + self._url(id, "invitations", invitaton_id), params=params + ) - def create_organization_invitation(self, id, body): + def create_organization_invitation( + self, id: str, body: dict[str, Any] + ) -> dict[str, Any]: """Create an invitation to an organization. Args: @@ -356,9 +454,9 @@ def create_organization_invitation(self, id, body): See: https://auth0.com/docs/api/management/v2#!/Organizations/post_invitations """ - return self.client.post(self._url(id, 'invitations'), data=body) + return self.client.post(self._url(id, "invitations"), data=body) - def delete_organization_invitation(self, id, invitation_id): + def delete_organization_invitation(self, id: str, invitation_id: str) -> Any: """Deletes an invitation from the given organization. Args: @@ -369,4 +467,66 @@ def delete_organization_invitation(self, id, invitation_id): See: https://auth0.com/docs/api/management/v2#!/Organizations/delete_invitations_by_invitation_id """ - return self.client.delete(self._url(id, 'invitations', invitation_id)) + return self.client.delete(self._url(id, "invitations", invitation_id)) + + def get_client_grants( + self, + id: str, + audience: str | None = None, + client_id: str | None = None, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): + """Get client grants associated to an organization. + + Args: + id (str): Id of organization. + + audience (str, optional): URL encoded audience of a Resource Server + to filter. + + client_id (string, optional): The id of a client to filter. + + page (int, optional): The result's page number (zero based). When not set, + the default value is up to the server. + + per_page (int, optional): The amount of entries per page. When not set, + the default value is up to the server. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + """ + params = { + "audience": audience, + "client_id": client_id, + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(id, "client-grants"), params=params) + + def add_client_grant(self, id: str, grant_id: str) -> dict[str, Any]: + """Associate a client grant with an organization. + + Args: + id (str): the ID of the organization. + + grant_id (string) A Client Grant ID to add to the organization. + """ + + return self.client.post( + self._url(id, "client-grants"), data={"grant_id": grant_id} + ) + + def delete_client_grant(self, id: str, grant_id: str) -> dict[str, Any]: + """Remove a client grant from an organization. + + Args: + id (str): the ID of the organization. + + grant_id (string) A Client Grant ID to remove from the organization. + """ + + return self.client.delete(self._url(id, "client-grants", grant_id)) diff --git a/auth0/v3/management/prompts.py b/auth0/management/prompts.py similarity index 54% rename from auth0/v3/management/prompts.py rename to auth0/management/prompts.py index 91108ab5..29fa07be 100644 --- a/auth0/v3/management/prompts.py +++ b/auth0/management/prompts.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Prompts(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Prompts: """Auth0 prompts endpoints Args: @@ -17,24 +22,37 @@ class Prompts(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, prompt=None, language=None): - url = "{}://{}/api/v2/prompts".format(self.protocol, self.domain) + def _url(self, prompt: str | None = None, language: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/prompts" if prompt is not None and language is not None: - return "{}/{}/custom-text/{}".format(url, prompt, language) + return f"{url}/{prompt}/custom-text/{language}" return url - def get(self): + def get(self) -> dict[str, Any]: """Retrieves prompts settings. See: https://auth0.com/docs/api/management/v2#!/Prompts/get_prompts @@ -42,7 +60,7 @@ def get(self): return self.client.get(self._url()) - def update(self, body): + def update(self, body: dict[str, Any]) -> dict[str, Any]: """Updates prompts settings. See: https://auth0.com/docs/api/management/v2#!/Prompts/patch_prompts @@ -50,17 +68,31 @@ def update(self, body): return self.client.patch(self._url(), data=body) - def get_custom_text(self, prompt, language): + def get_custom_text(self, prompt: str, language: str): """Retrieves custom text for a prompt in a specific language. + Args: + prompt (str): Name of the prompt. + + language (str): Language to update. + See: https://auth0.com/docs/api/management/v2#!/Prompts/get_custom_text_by_language """ return self.client.get(self._url(prompt, language)) - def update_custom_text(self, prompt, language, body): + def update_custom_text( + self, prompt: str, language: str, body: dict[str, Any] + ) -> dict[str, Any]: """Updates custom text for a prompt in a specific language. + Args: + prompt (str): Name of the prompt. + + language (str): Language to update. + + body (dict): An object containing custom dictionaries for a group of screens. + See: https://auth0.com/docs/api/management/v2#!/Prompts/put_custom_text_by_language """ diff --git a/auth0/v3/management/resource_servers.py b/auth0/management/resource_servers.py similarity index 67% rename from auth0/v3/management/resource_servers.py rename to auth0/management/resource_servers.py index 61ca4e9a..a71d1378 100644 --- a/auth0/v3/management/resource_servers.py +++ b/auth0/management/resource_servers.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class ResourceServers(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class ResourceServers: """Auth0 resource servers endpoints Args: @@ -17,24 +22,37 @@ class ResourceServers(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/resource-servers'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/resource-servers" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Create a new resource server. Args: @@ -45,7 +63,12 @@ def create(self, body): return self.client.post(self._url(), data=body) - def get_all(self, page=None, per_page=None, include_totals=False): + def get_all( + self, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): """Retrieves all resource servers Args: @@ -63,14 +86,14 @@ def get_all(self, page=None, per_page=None, include_totals=False): """ params = { - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower() + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), } return self.client.get(self._url(), params=params) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Retrieves a resource server by its id. Args: @@ -82,7 +105,7 @@ def get(self, id): return self.client.get(self._url(id)) - def delete(self, id): + def delete(self, id: str) -> Any: """Deletes a resource server. Args: @@ -94,7 +117,7 @@ def delete(self, id): return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Modifies a resource server. Args: diff --git a/auth0/v3/management/roles.py b/auth0/management/roles.py similarity index 67% rename from auth0/v3/management/roles.py rename to auth0/management/roles.py index 700e0d70..ca33430c 100644 --- a/auth0/v3/management/roles.py +++ b/auth0/management/roles.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any, List # List is being used as list is already a method. -class Roles(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Roles: """Auth0 roles endpoints Args: @@ -17,24 +22,43 @@ class Roles(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/roles'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/roles" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def list(self, page=0, per_page=25, include_totals=True, name_filter=None): + def list( + self, + page: int = 0, + per_page: int = 25, + include_totals: bool = True, + name_filter: str | None = None, + ): """List or search roles. Args: @@ -53,14 +77,14 @@ def list(self, page=0, per_page=25, include_totals=True, name_filter=None): See: https://auth0.com/docs/api/management/v2#!/Roles/get_roles """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower(), - 'name_filter': name_filter + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "name_filter": name_filter, } return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new role. Args: @@ -70,7 +94,7 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Get a role. Args: @@ -81,7 +105,7 @@ def get(self, id): return self.client.get(self._url(id)) - def delete(self, id): + def delete(self, id: str) -> Any: """Delete a role. Args: @@ -91,7 +115,7 @@ def delete(self, id): """ return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update a role with the attributes passed in 'body' Args: @@ -103,7 +127,15 @@ def update(self, id, body): """ return self.client.patch(self._url(id), data=body) - def list_users(self, id, page=0, per_page=25, include_totals=True, from_param=None, take=None): + def list_users( + self, + id: str, + page: int = 0, + per_page: int = 25, + include_totals: bool = True, + from_param: str | None = None, + take: int | None = None, + ): """List the users that have been associated with a given role. Args: @@ -128,17 +160,17 @@ def list_users(self, id, page=0, per_page=25, include_totals=True, from_param=No """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower(), - 'from': from_param, - 'take': take + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "from": from_param, + "take": take, } - url = self._url('{}/users'.format(id)) + url = self._url(f"{id}/users") return self.client.get(url, params=params) - def add_users(self, id, users): + def add_users(self, id: str, users: List[str]) -> dict[str, Any]: """Assign users to a role. Args: @@ -148,11 +180,13 @@ def add_users(self, id, users): See https://auth0.com/docs/api/management/v2#!/Roles/post_role_users """ - url = self._url('{}/users'.format(id)) - body = {'users': users} + url = self._url(f"{id}/users") + body = {"users": users} return self.client.post(url, data=body) - def list_permissions(self, id, page=0, per_page=25, include_totals=True): + def list_permissions( + self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True + ): """List the permissions associated to a role. Args: @@ -170,14 +204,14 @@ def list_permissions(self, id, page=0, per_page=25, include_totals=True): See https://auth0.com/docs/api/management/v2#!/Roles/get_role_permission """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower() + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), } - url = self._url('{}/permissions'.format(id)) + url = self._url(f"{id}/permissions") return self.client.get(url, params=params) - def remove_permissions(self, id, permissions): + def remove_permissions(self, id: str, permissions: List[dict[str, str]]) -> Any: """Unassociates permissions from a role. Args: @@ -187,11 +221,11 @@ def remove_permissions(self, id, permissions): See https://auth0.com/docs/api/management/v2#!/Roles/delete_role_permission_assignment """ - url = self._url('{}/permissions'.format(id)) - body = {'permissions': permissions} + url = self._url(f"{id}/permissions") + body = {"permissions": permissions} return self.client.delete(url, data=body) - def add_permissions(self, id, permissions): + def add_permissions(self, id: str, permissions: List[dict[str, str]]) -> dict[str, Any]: """Associates permissions with a role. Args: @@ -201,6 +235,6 @@ def add_permissions(self, id, permissions): See https://auth0.com/docs/api/management/v2#!/Roles/post_role_permission_assignment """ - url = self._url('{}/permissions'.format(id)) - body = {'permissions': permissions} + url = self._url(f"{id}/permissions") + body = {"permissions": permissions} return self.client.post(url, data=body) diff --git a/auth0/v3/management/rules.py b/auth0/management/rules.py similarity index 66% rename from auth0/v3/management/rules.py rename to auth0/management/rules.py index cf70c87a..9b0b5d14 100644 --- a/auth0/v3/management/rules.py +++ b/auth0/management/rules.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Rules(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Rules: """Rules endpoint implementation. Args: @@ -17,25 +22,46 @@ class Rules(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/rules'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/rules" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def all(self, stage='login_success', enabled=True, fields=None, - include_fields=True, page=None, per_page=None, include_totals=False): + def all( + self, + stage: str = "login_success", + enabled: bool = True, + fields: list[str] | None = None, + include_fields: bool = True, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): """Retrieves a list of all rules. Args: @@ -65,21 +91,21 @@ def all(self, stage='login_success', enabled=True, fields=None, """ params = { - 'stage': stage, - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower(), - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower() + "stage": stage, + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), } # since the default is True, this is here to disable the filter if enabled is not None: - params['enabled'] = str(enabled).lower() + params["enabled"] = str(enabled).lower() return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new rule. Args: @@ -89,7 +115,9 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def get(self, id, fields=None, include_fields=True): + def get( + self, id: str, fields: list[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Retrieves a rule by its ID. Args: @@ -104,11 +132,13 @@ def get(self, id, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Rules/get_rules_by_id """ - params = {'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower()} + params = { + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + } return self.client.get(self._url(id), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Delete a rule. Args: @@ -118,7 +148,7 @@ def delete(self, id): """ return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update an existing rule Args: diff --git a/auth0/v3/management/rules_configs.py b/auth0/management/rules_configs.py similarity index 62% rename from auth0/v3/management/rules_configs.py rename to auth0/management/rules_configs.py index 04ddb659..669f62aa 100644 --- a/auth0/v3/management/rules_configs.py +++ b/auth0/management/rules_configs.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class RulesConfigs(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class RulesConfigs: """RulesConfig endpoint implementation. Args: @@ -17,31 +22,44 @@ class RulesConfigs(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/rules-configs'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/rules-configs" if id is not None: - return url + '/' + id + return url + "/" + id return url - def all(self): + def all(self) -> list[dict[str, Any]]: """Lists the config variable keys for rules. See: https://auth0.com/docs/api/management/v2#!/Rules_Configs/get_rules_configs """ return self.client.get(self._url()) - def unset(self, key): + def unset(self, key: str) -> Any: """Removes the rules config for a given key. Args: @@ -51,7 +69,7 @@ def unset(self, key): """ return self.client.delete(self._url(key)) - def set(self, key, value): + def set(self, key: str, value: str) -> dict[str, Any]: """Sets the rules config for a given key. Args: @@ -61,6 +79,6 @@ def set(self, key, value): See: https://auth0.com/docs/api/management/v2#!/Rules_Configs/put_rules_configs_by_key """ - url = self._url('{}'.format(key)) - body = {'value': value} + url = self._url(f"{key}") + body = {"value": value} return self.client.put(url, data=body) diff --git a/auth0/management/self_service_profiles.py b/auth0/management/self_service_profiles.py new file mode 100644 index 00000000..a9a52610 --- /dev/null +++ b/auth0/management/self_service_profiles.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +from typing import Any, List # List is being used as list is already a method. + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class SelfServiceProfiles: + """Auth0 Self Service Profiles endpoints + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, profile_id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/self-service-profiles" + if profile_id is not None: + return f"{url}/{profile_id}" + return url + + def all( + self, + page: int = 0, + per_page: int = 25, + include_totals: bool = True, + ) -> List[dict[str, Any]]: + """List self-service profiles. + + Args: + page (int, optional): The result's page number (zero based). By default, + retrieves the first page of results. + + per_page (int, optional): The amount of entries per page. By default, + retrieves 25 results per page. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to True. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles + """ + + params = { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(), params=params) + + def create(self, body: dict[str, Any]) -> dict[str, Any]: + """Create a new self-service profile. + + Args: + body (dict): Attributes for the new self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-self-service-profiles + """ + + return self.client.post(self._url(), data=body) + + def get(self, profile_id: str) -> dict[str, Any]: + """Get a self-service profile. + + Args: + id (str): The id of the self-service profile to retrieve. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles-by-id + """ + + return self.client.get(self._url(profile_id)) + + def delete(self, profile_id: str) -> None: + """Delete a self-service profile. + + Args: + id (str): The id of the self-service profile to delete. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/delete-self-service-profiles-by-id + """ + + self.client.delete(self._url(profile_id)) + + def update(self, profile_id: str, body: dict[str, Any]) -> dict[str, Any]: + """Update a self-service profile. + + Args: + id (str): The id of the self-service profile to update. + + body (dict): Attributes of the self-service profile to modify. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/patch-self-service-profiles-by-id + """ + + return self.client.patch(self._url(profile_id), data=body) + + def get_custom_text( + self, profile_id: str, language: str, page: str + ) -> dict[str, Any]: + """Get the custom text for a self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profile-custom-text + """ + + url = self._url(f"{profile_id}/custom-text/{language}/{page}") + return self.client.get(url) + + def update_custom_text( + self, profile_id: str, language: str, page: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Update the custom text for a self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/put-self-service-profile-custom-text + """ + + url = self._url(f"{profile_id}/custom-text/{language}/{page}") + return self.client.put(url, data=body) + + def create_sso_ticket( + self, profile_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Create a single sign-on ticket for a self-service profile. + + Args: + id (str): The id of the self-service profile to create the ticket for. + + body (dict): Attributes for the single sign-on ticket. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-sso-ticket + """ + + url = self._url(f"{profile_id}/sso-ticket") + return self.client.post(url, data=body) + + def revoke_sso_ticket(self, profile_id: str, ticket_id: str) -> None: + """Revoke a single sign-on ticket for a self-service profile. + + Args: + id (str): The id of the self-service profile to revoke the ticket from. + + ticket (str): The ticket to revoke. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-revoke + """ + + url = self._url(f"{profile_id}/sso-ticket/{ticket_id}/revoke") + self.client.post(url) \ No newline at end of file diff --git a/auth0/v3/management/stats.py b/auth0/management/stats.py similarity index 55% rename from auth0/v3/management/stats.py rename to auth0/management/stats.py index a30d3611..486f4408 100644 --- a/auth0/v3/management/stats.py +++ b/auth0/management/stats.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Stats(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Stats: """Auth0 stats endpoints Args: @@ -17,21 +22,34 @@ class Stats(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, action): - return '{}://{}/api/v2/stats/{}'.format(self.protocol, self.domain, action) + def _url(self, action: str) -> str: + return f"{self.protocol}://{self.domain}/api/v2/stats/{action}" - def active_users(self): + def active_users(self) -> int: """Gets the active users count (logged in during the last 30 days). Returns: An integer. @@ -39,9 +57,11 @@ def active_users(self): See: https://auth0.com/docs/api/management/v2#!/Stats/get_active_users """ - return self.client.get(self._url('active-users')) + return self.client.get(self._url("active-users")) - def daily_stats(self, from_date=None, to_date=None): + def daily_stats( + self, from_date: str | None = None, to_date: str | None = None + ) -> list[dict[str, Any]]: """Gets the daily stats for a particular period. Args: @@ -54,5 +74,6 @@ def daily_stats(self, from_date=None, to_date=None): See: https://auth0.com/docs/api/management/v2#!/Stats/get_daily """ - return self.client.get(self._url('daily'), params={'from': from_date, - 'to': to_date}) + return self.client.get( + self._url("daily"), params={"from": from_date, "to": to_date} + ) diff --git a/auth0/v3/management/tenants.py b/auth0/management/tenants.py similarity index 59% rename from auth0/v3/management/tenants.py rename to auth0/management/tenants.py index 958c7230..b2f39867 100644 --- a/auth0/v3/management/tenants.py +++ b/auth0/management/tenants.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Tenants(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Tenants: """Auth0 tenants endpoints Args: @@ -17,21 +22,36 @@ class Tenants(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self): - return '{}://{}/api/v2/tenants/settings'.format(self.protocol, self.domain) + def _url(self) -> str: + return f"{self.protocol}://{self.domain}/api/v2/tenants/settings" - def get(self, fields=None, include_fields=True): + def get( + self, fields: list[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Get tenant settings. Args: @@ -45,12 +65,14 @@ def get(self, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Tenants/get_settings """ - params = {'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower()} + params = { + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + } return self.client.get(self._url(), params=params) - def update(self, body): + def update(self, body: dict[str, Any]) -> dict[str, Any]: """Update tenant settings. Args: diff --git a/auth0/v3/management/tickets.py b/auth0/management/tickets.py similarity index 54% rename from auth0/v3/management/tickets.py rename to auth0/management/tickets.py index 004a9510..f44e44e0 100644 --- a/auth0/v3/management/tickets.py +++ b/auth0/management/tickets.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class Tickets(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class Tickets: """Auth0 tickets endpoints Args: @@ -17,21 +22,34 @@ class Tickets(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, action): - return '{}://{}/api/v2/tickets/{}'.format(self.protocol, self.domain, action) + def _url(self, action: str) -> str: + return f"{self.protocol}://{self.domain}/api/v2/tickets/{action}" - def create_email_verification(self, body): + def create_email_verification(self, body: dict[str, Any]) -> dict[str, Any]: """Create an email verification ticket. Args: @@ -39,9 +57,9 @@ def create_email_verification(self, body): See: https://auth0.com/docs/api/v2#!/Tickets/post_email_verification """ - return self.client.post(self._url('email-verification'), data=body) + return self.client.post(self._url("email-verification"), data=body) - def create_pswd_change(self, body): + def create_pswd_change(self, body: dict[str, Any]) -> dict[str, Any]: """Create password change ticket. Args: @@ -49,4 +67,4 @@ def create_pswd_change(self, body): See: https://auth0.com/docs/api/v2#!/Tickets/post_password_change """ - return self.client.post(self._url('password-change'), data=body) + return self.client.post(self._url("password-change"), data=body) diff --git a/auth0/v3/management/user_blocks.py b/auth0/management/user_blocks.py similarity index 63% rename from auth0/v3/management/user_blocks.py rename to auth0/management/user_blocks.py index 7df1c82a..279dc5d9 100644 --- a/auth0/v3/management/user_blocks.py +++ b/auth0/management/user_blocks.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class UserBlocks(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class UserBlocks: """Auth0 user blocks endpoints Args: @@ -17,24 +22,37 @@ class UserBlocks(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/user-blocks'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/user-blocks" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def get_by_identifier(self, identifier): + def get_by_identifier(self, identifier: str) -> dict[str, Any]: """Gets blocks by identifier Args: @@ -43,11 +61,11 @@ def get_by_identifier(self, identifier): See: https://auth0.com/docs/api/management/v2#!/User_Blocks/get_user_blocks """ - params = {'identifier': identifier} + params = {"identifier": identifier} return self.client.get(self._url(), params=params) - def unblock_by_identifier(self, identifier): + def unblock_by_identifier(self, identifier: dict[str, Any]) -> Any: """Unblocks by identifier Args: @@ -56,11 +74,11 @@ def unblock_by_identifier(self, identifier): See: https://auth0.com/docs/api/management/v2#!/User_Blocks/delete_user_blocks """ - params = {'identifier': identifier} + params = {"identifier": identifier} return self.client.delete(self._url(), params=params) - def get(self, id): + def get(self, id: str) -> dict[str, Any]: """Get a user's blocks Args: @@ -71,7 +89,7 @@ def get(self, id): return self.client.get(self._url(id)) - def unblock(self, id): + def unblock(self, id: str) -> Any: """Unblock a user Args: diff --git a/auth0/v3/management/users.py b/auth0/management/users.py similarity index 50% rename from auth0/v3/management/users.py rename to auth0/management/users.py index fb8f2c19..2fd9a46a 100644 --- a/auth0/v3/management/users.py +++ b/auth0/management/users.py @@ -1,9 +1,12 @@ -import warnings +from __future__ import annotations -from .rest import RestClient +from typing import Any, List # List is being used as list is already a method. +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType -class Users(object): + +class Users: """Auth0 users endpoints Args: @@ -19,26 +22,48 @@ class Users(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self, id=None): - url = '{}://{}/api/v2/users'.format(self.protocol, self.domain) + def _url(self, id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/users" if id is not None: - return '{}/{}'.format(url, id) + return f"{url}/{id}" return url - def list(self, page=0, per_page=25, sort=None, connection=None, q=None, - search_engine=None, include_totals=True, fields=None, - include_fields=True): + def list( + self, + page: int = 0, + per_page: int = 25, + sort: str | None = None, + connection: str | None = None, + q: str | None = None, + search_engine: str | None = None, + include_totals: bool = True, + fields: List[str] | None = None, + include_fields: bool = True, + ): """List or search users. Args: @@ -75,19 +100,19 @@ def list(self, page=0, per_page=25, sort=None, connection=None, q=None, See: https://auth0.com/docs/api/management/v2#!/Users/get_users """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower(), - 'sort': sort, - 'connection': connection, - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower(), - 'q': q, - 'search_engine': search_engine + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "sort": sort, + "connection": connection, + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), + "q": q, + "search_engine": search_engine, } return self.client.get(self._url(), params=params) - def create(self, body): + def create(self, body: dict[str, Any]) -> dict[str, Any]: """Creates a new user. Args: @@ -97,16 +122,9 @@ def create(self, body): """ return self.client.post(self._url(), data=body) - def delete_all_users(self): - """Deletes all users (USE WITH CAUTION). - Deprecation: This endpoint is no longer available server-side. - - Args: - """ - warnings.warn("DELETE all users endpoint is no longer available.", DeprecationWarning) - return self.client.delete(self._url()) - - def get(self, id, fields=None, include_fields=True): + def get( + self, id: str, fields: List[str] | None = None, include_fields: bool = True + ) -> dict[str, Any]: """Get a user. Args: @@ -122,13 +140,13 @@ def get(self, id, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id """ params = { - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower() + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), } return self.client.get(self._url(id), params=params) - def delete(self, id): + def delete(self, id: str) -> Any: """Delete a user. Args: @@ -138,7 +156,7 @@ def delete(self, id): """ return self.client.delete(self._url(id)) - def update(self, id, body): + def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """Update a user with the attributes passed in 'body' Args: @@ -150,7 +168,9 @@ def update(self, id, body): """ return self.client.patch(self._url(id), data=body) - def list_organizations(self, id, page=0, per_page=25, include_totals=True): + def list_organizations( + self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True + ): """List the organizations that the user is member of. Args: @@ -168,15 +188,17 @@ def list_organizations(self, id, page=0, per_page=25, include_totals=True): See https://auth0.com/docs/api/management/v2#!/Users/get_organizations """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower() + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), } - url = self._url('{}/organizations'.format(id)) + url = self._url(f"{id}/organizations") return self.client.get(url, params=params) - def list_roles(self, id, page=0, per_page=25, include_totals=True): + def list_roles( + self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True + ): """List the roles associated with a user. Args: @@ -194,15 +216,15 @@ def list_roles(self, id, page=0, per_page=25, include_totals=True): See https://auth0.com/docs/api/management/v2#!/Users/get_user_roles """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower() + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), } - url = self._url('{}/roles'.format(id)) + url = self._url(f"{id}/roles") return self.client.get(url, params=params) - def remove_roles(self, id, roles): + def remove_roles(self, id: str, roles: List[str]) -> Any: """Removes an array of roles from a user. Args: @@ -212,11 +234,11 @@ def remove_roles(self, id, roles): See https://auth0.com/docs/api/management/v2#!/Users/delete_user_roles """ - url = self._url('{}/roles'.format(id)) - body = {'roles': roles} + url = self._url(f"{id}/roles") + body = {"roles": roles} return self.client.delete(url, data=body) - def add_roles(self, id, roles): + def add_roles(self, id: str, roles: List[str]) -> dict[str, Any]: """Associate an array of roles with a user. Args: @@ -226,11 +248,13 @@ def add_roles(self, id, roles): See https://auth0.com/docs/api/management/v2#!/Users/post_user_roles """ - url = self._url('{}/roles'.format(id)) - body = {'roles': roles} + url = self._url(f"{id}/roles") + body = {"roles": roles} return self.client.post(url, data=body) - def list_permissions(self, id, page=0, per_page=25, include_totals=True): + def list_permissions( + self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True + ): """List the permissions associated to the user. Args: @@ -249,14 +273,14 @@ def list_permissions(self, id, page=0, per_page=25, include_totals=True): """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower() + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), } - url = self._url('{}/permissions'.format(id)) + url = self._url(f"{id}/permissions") return self.client.get(url, params=params) - def remove_permissions(self, id, permissions): + def remove_permissions(self, id: str, permissions: List[str]) -> Any: """Removes permissions from a user. Args: @@ -266,11 +290,11 @@ def remove_permissions(self, id, permissions): See https://auth0.com/docs/api/management/v2#!/Users/delete_permissions """ - url = self._url('{}/permissions'.format(id)) - body = {'permissions': permissions} + url = self._url(f"{id}/permissions") + body = {"permissions": permissions} return self.client.delete(url, data=body) - def add_permissions(self, id, permissions): + def add_permissions(self, id: str, permissions: List[str]) -> dict[str, Any]: """Assign permissions to a user. Args: @@ -280,11 +304,11 @@ def add_permissions(self, id, permissions): See https://auth0.com/docs/api/management/v2#!/Users/post_permissions """ - url = self._url('{}/permissions'.format(id)) - body = {'permissions': permissions} + url = self._url(f"{id}/permissions") + body = {"permissions": permissions} return self.client.post(url, data=body) - def delete_multifactor(self, id, provider): + def delete_multifactor(self, id: str, provider: str) -> Any: """Delete a user's multifactor provider. Args: @@ -295,10 +319,10 @@ def delete_multifactor(self, id, provider): See: https://auth0.com/docs/api/management/v2#!/Users/delete_multifactor_by_provider """ - url = self._url('{}/multifactor/{}'.format(id, provider)) + url = self._url(f"{id}/multifactor/{provider}") return self.client.delete(url) - def delete_authenticators(self, id): + def delete_authenticators(self, id: str) -> Any: """Delete a user's MFA enrollments. Args: @@ -306,10 +330,10 @@ def delete_authenticators(self, id): See: https://auth0.com/docs/api/management/v2#!/Users/delete_authenticators """ - url = self._url('{}/authenticators'.format(id)) + url = self._url(f"{id}/authenticators") return self.client.delete(url) - def unlink_user_account(self, id, provider, user_id): + def unlink_user_account(self, id: str, provider: str, user_id: str) -> Any: """Unlink a user account Args: @@ -321,10 +345,10 @@ def unlink_user_account(self, id, provider, user_id): See: https://auth0.com/docs/api/management/v2#!/Users/delete_user_identity_by_user_id """ - url = self._url('{}/identities/{}/{}'.format(id, provider, user_id)) + url = self._url(f"{id}/identities/{provider}/{user_id}") return self.client.delete(url) - def link_user_account(self, user_id, body): + def link_user_account(self, user_id: str, body: dict[str, Any]) -> list[dict[str, Any]]: """Link user accounts. Links the account specified in the body (secondary account) to the @@ -338,10 +362,10 @@ def link_user_account(self, user_id, body): See: https://auth0.com/docs/api/v2#!/Users/post_identities """ - url = self._url('{}/identities'.format(user_id)) + url = self._url(f"{user_id}/identities") return self.client.post(url, data=body) - def regenerate_recovery_code(self, user_id): + def regenerate_recovery_code(self, user_id: str) -> dict[str, Any]: """Removes the current recovery token, generates and returns a new one Args: @@ -349,22 +373,28 @@ def regenerate_recovery_code(self, user_id): See: https://auth0.com/docs/api/management/v2#!/Users/post_recovery_code_regeneration """ - url = self._url('{}/recovery-code-regeneration'.format(user_id)) + url = self._url(f"{user_id}/recovery-code-regeneration") return self.client.post(url) - def get_guardian_enrollments(self, user_id): - """Retrieves all Guardian enrollments. + def get_guardian_enrollments(self, user_id: str) -> dict[str, Any]: + """Retrieve the first confirmed Guardian enrollment for a user. Args: user_id (str): The user_id of the user to retrieve. See: https://auth0.com/docs/api/management/v2#!/Users/get_enrollments """ - url = self._url('{}/enrollments'.format(user_id)) + url = self._url(f"{user_id}/enrollments") return self.client.get(url) - def get_log_events(self, user_id, page=0, per_page=50, sort=None, - include_totals=False): + def get_log_events( + self, + user_id: str, + page: int = 0, + per_page: int = 50, + sort: str | None = None, + include_totals: bool = False, + ): """Retrieve every log event for a specific user id. Args: @@ -388,16 +418,16 @@ def get_log_events(self, user_id, page=0, per_page=50, sort=None, """ params = { - 'per_page': per_page, - 'page': page, - 'include_totals': str(include_totals).lower(), - 'sort': sort + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "sort": sort, } - url = self._url('{}/logs'.format(user_id)) + url = self._url(f"{user_id}/logs") return self.client.get(url, params=params) - def invalidate_remembered_browsers(self, user_id): + def invalidate_remembered_browsers(self, user_id: str) -> dict[str, Any]: """Invalidate all remembered browsers across all authentication factors for a user. Args: @@ -406,5 +436,148 @@ def invalidate_remembered_browsers(self, user_id): See: https://auth0.com/docs/api/management/v2#!/Users/post_invalidate_remember_browser """ - url = self._url('{}/multifactor/actions/invalidate-remember-browser'.format(user_id)) + url = self._url(f"{user_id}/multifactor/actions/invalidate-remember-browser") return self.client.post(url) + + def get_authentication_methods(self, user_id: str) -> dict[str, Any]: + """Gets a list of authentication methods + + Args: + user_id (str): The user_id to get a list of authentication methods for. + + See: https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.get(url) + + def get_authentication_method_by_id( + self, user_id: str, authentication_method_id: str + ) -> dict[str, Any]: + """Gets an authentication method by ID. + + Args: + user_id (str): The user_id to get an authentication method by ID for. + authentication_method_id (str): The authentication_method_id to get an authentication method by ID for. + + See: https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.get(url) + + def create_authentication_method( + self, user_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Creates an authentication method for a given user. + + Args: + user_id (str): The user_id to create an authentication method for a given user. + body (dict): the request body to create an authentication method for a given user. + + See: https://auth0.com/docs/api/management/v2#!/Users/post_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.post(url, data=body) + + def update_authentication_methods( + self, user_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Updates all authentication methods for a user by replacing them with the given ones. + + Args: + user_id (str): The user_id to update all authentication methods for. + body (dict): the request body to update all authentication methods with. + + See: https://auth0.com/docs/api/management/v2#!/Users/put_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.put(url, data=body) + + def update_authentication_method_by_id( + self, user_id: str, authentication_method_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Updates an authentication method. + + Args: + user_id (str): The user_id to update an authentication method. + authentication_method_id (str): The authentication_method_id to update an authentication method for. + body (dict): the request body to update an authentication method. + + See: https://auth0.com/docs/api/management/v2#!/Users/patch_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.patch(url, data=body) + + def delete_authentication_methods(self, user_id: str) -> Any: + """Deletes all authentication methods for the given user. + + Args: + user_id (str): The user_id to delete all authentication methods for the given user for. + + See: https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.delete(url) + + def delete_authentication_method_by_id( + self, user_id: str, authentication_method_id: str + ) -> Any: + """Deletes an authentication method by ID. + + Args: + user_id (str): The user_id to delete an authentication method by ID for. + authentication_method_id (str): The authentication_method_id to delete an authentication method by ID for. + + See: https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.delete(url) + + def list_tokensets( + self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True + ): + """List all the tokenset(s) associated to the user. + + Args: + id (str): The user's id. + + page (int, optional): The result's page number (zero based). By default, + retrieves the first page of results. + + per_page (int, optional): The amount of entries per page. By default, + retrieves 25 results per page. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to True. + + See https://auth0.com/docs/api/management/v2#!/Users/get_tokensets + """ + + params = { + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + } + url = self._url(f"{id}/federated-connections-tokensets") + return self.client.get(url, params=params) + + def delete_tokenset_by_id( + self, user_id: str, tokenset_id: str + ) -> Any: + """Deletes an tokenset by ID. + + Args: + user_id (str): The user_id to delete an authentication method by ID for. + tokenset_id (str): The tokenset_id to delete an tokenset by ID for. + + See: https://auth0.com/docs/api/management/v2#!/Users/delete_tokenset_by_id + """ + + url = self._url(f"{user_id}/federated-connections-tokensets/{tokenset_id}") + return self.client.delete(url) \ No newline at end of file diff --git a/auth0/v3/management/users_by_email.py b/auth0/management/users_by_email.py similarity index 57% rename from auth0/v3/management/users_by_email.py rename to auth0/management/users_by_email.py index 26f98ab8..009ca8aa 100644 --- a/auth0/v3/management/users_by_email.py +++ b/auth0/management/users_by_email.py @@ -1,7 +1,12 @@ -from .rest import RestClient +from __future__ import annotations +from typing import Any -class UsersByEmail(object): +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class UsersByEmail: """Auth0 users by email endpoints Args: @@ -17,21 +22,36 @@ class UsersByEmail(object): both values separately or a float to set both to it. (defaults to 5.0 for both) + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + rest_options (RestClientOptions): Pass an instance of RestClientOptions to configure additional RestClient options, such as rate-limit retries. (defaults to None) """ - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: self.domain = domain self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) - def _url(self): - return '{}://{}/api/v2/users-by-email'.format(self.protocol, self.domain) + def _url(self) -> str: + return f"{self.protocol}://{self.domain}/api/v2/users-by-email" - def search_users_by_email(self, email, fields=None, include_fields=True): + def search_users_by_email( + self, email: str, fields: list[str] | None = None, include_fields: bool = True + ) -> list[dict[str, Any]]: """List or search users. Args: @@ -48,8 +68,8 @@ def search_users_by_email(self, email, fields=None, include_fields=True): See: https://auth0.com/docs/api/management/v2#!/Users_By_Email/get_users_by_email """ params = { - 'email': email, - 'fields': fields and ','.join(fields) or None, - 'include_fields': str(include_fields).lower() + "email": email, + "fields": fields and ",".join(fields) or None, + "include_fields": str(include_fields).lower(), } return self.client.get(self._url(), params=params) diff --git a/auth0/rest.py b/auth0/rest.py new file mode 100644 index 00000000..a2d9bd9a --- /dev/null +++ b/auth0/rest.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import base64 +import platform +import sys +from json import dumps, loads +from random import randint +from time import sleep +from typing import TYPE_CHECKING, Any, Mapping +from urllib.parse import urlencode + +import requests + +from auth0.exceptions import Auth0Error, RateLimitError +from auth0.types import RequestData, TimeoutType + +if TYPE_CHECKING: + from auth0.rest_async import RequestsResponse + +UNKNOWN_ERROR = "a0.sdk.internal.unknown" + + +class RestClientOptions: + """Configuration object for RestClient. Used for configuring + additional RestClient options, such as rate-limit + retries. + + Args: + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + retries (integer): In the event an API request returns a + 429 response header (indicating rate-limit has been + hit), the RestClient will retry the request this many + times using an exponential backoff strategy, before + raising a RateLimitError exception. 10 retries max. + (defaults to 3) + """ + + def __init__( + self, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + retries: int = 3, + ) -> None: + self.telemetry = telemetry + self.timeout = timeout + self.retries = retries + + +class RestClient: + """Provides simple methods for handling all RESTful api endpoints. + + Args: + jwt (str, optional): The JWT to be used with the RestClient. + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. Overrides matching + options passed to the constructor. + (defaults to 3) + """ + + def __init__( + self, + jwt: str | None, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + options: RestClientOptions | None = None, + ) -> None: + if options is None: + options = RestClientOptions(telemetry=telemetry, timeout=timeout) + + self.options = options + self.jwt = jwt + + self._metrics = {"retries": 0, "backoff": []} + self._skip_sleep = False + + self.base_headers = { + "Content-Type": "application/json", + } + + if jwt is not None: + self.base_headers["Authorization"] = f"Bearer {self.jwt}" + + if options.telemetry: + py_version = platform.python_version() + version = sys.modules["auth0"].__version__ + + auth0_client = dumps( + { + "name": "auth0-python", + "version": version, + "env": { + "python": py_version, + }, + } + ).encode("utf-8") + + self.base_headers.update( + { + "User-Agent": f"Python/{py_version}", + "Auth0-Client": base64.b64encode(auth0_client).decode(), + } + ) + + # Cap the maximum number of retries to 10 or fewer. Floor the retries at 0. + self._retries = min(self.MAX_REQUEST_RETRIES(), max(0, options.retries)) + + # For backwards compatibility reasons only + # TODO: Deprecate in the next major so we can prune these arguments. Guidance should be to use RestClient.options.* + self.telemetry = options.telemetry + self.timeout = options.timeout + + # Returns a hard cap for the maximum number of retries allowed (10) + def MAX_REQUEST_RETRIES(self) -> int: + return 10 + + # Returns the maximum amount of jitter to introduce in milliseconds (100ms) + def MAX_REQUEST_RETRY_JITTER(self) -> int: + return 100 + + # Returns the maximum delay window allowed (1000ms) + def MAX_REQUEST_RETRY_DELAY(self) -> int: + return 1000 + + # Returns the minimum delay window allowed (100ms) + def MIN_REQUEST_RETRY_DELAY(self) -> int: + return 100 + + def _request( + self, + method: str, + url: str, + params: dict[str, Any] | None = None, + data: RequestData | None = None, + json: RequestData | None = None, + headers: dict[str, str] | None = None, + files: dict[str, Any] | None = None, + ) -> Any: + # Track the API request attempt number + attempt = 0 + + # Reset the metrics tracker + self._metrics = {"retries": 0, "backoff": []} + + if data is None and json is not None and headers: + content_type = headers.get("Content-Type", "").lower() # Get Content-Type + if "application/x-www-form-urlencoded" in content_type: + data = urlencode(json) # Copy JSON data into data + json = None # Prevent JSON from being sent + + kwargs = { + k: v + for k, v in { + "params": params, + "json": json, + "data": data, + "headers": headers, + "files": files, + "timeout": self.options.timeout, + }.items() + if v is not None + } + + while True: + # Increment attempt number + attempt += 1 + + # Issue the request + response = requests.request(method, url, **kwargs) + + # If the response did not have a 429 header, or the attempt number is greater than the configured retries, break + if response.status_code != 429 or attempt > self._retries: + break + + wait = self._calculate_wait(attempt) + + # Skip calling sleep() when running unit tests + if self._skip_sleep is False: + # sleep() functions in seconds, so convert the milliseconds formula above accordingly + sleep(wait / 1000) + + # Return the final Response + return self._process_response(response) + + def get( + self, + url: str, + params: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + request_headers = self.base_headers.copy() + request_headers.update(headers or {}) + return self._request("GET", url, params=params, headers=request_headers) + + def post( + self, + url: str, + data: RequestData | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + request_headers = self.base_headers.copy() + request_headers.update(headers or {}) + return self._request("POST", url, json=data, headers=request_headers) + + def file_post( + self, + url: str, + data: RequestData | None = None, + files: dict[str, Any] | None = None, + ) -> Any: + headers = self.base_headers.copy() + headers.pop("Content-Type", None) + return self._request("POST", url, data=data, files=files, headers=headers) + + def patch(self, url: str, data: RequestData | None = None) -> Any: + headers = self.base_headers.copy() + return self._request("PATCH", url, json=data, headers=headers) + + def put(self, url: str, data: RequestData | None = None) -> Any: + headers = self.base_headers.copy() + return self._request("PUT", url, json=data, headers=headers) + + def delete( + self, + url: str, + params: dict[str, Any] | None = None, + data: RequestData | None = None, + ) -> Any: + headers = self.base_headers.copy() + return self._request("DELETE", url, params=params, json=data, headers=headers) + + def _calculate_wait(self, attempt: int) -> int: + # Retry the request. Apply a exponential backoff for subsequent attempts, using this formula: + # max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (100ms * (2 ** attempt - 1)) + random_between(1, MAX_REQUEST_RETRY_JITTER))) + + # Increases base delay by (100ms * (2 ** attempt - 1)) + wait = 100 * 2 ** (attempt - 1) + + # Introduces jitter to the base delay; increases delay between 1ms to MAX_REQUEST_RETRY_JITTER (100ms) + wait += randint(1, self.MAX_REQUEST_RETRY_JITTER()) + + # Is never more than MAX_REQUEST_RETRY_DELAY (1s) + wait = min(self.MAX_REQUEST_RETRY_DELAY(), wait) + + # Is never less than MIN_REQUEST_RETRY_DELAY (100ms) + wait = max(self.MIN_REQUEST_RETRY_DELAY(), wait) + + self._metrics["retries"] = attempt + self._metrics["backoff"].append(wait) # type: ignore[attr-defined] + + return wait + + def _process_response(self, response: requests.Response) -> Any: + return self._parse(response).content() + + def _parse(self, response: requests.Response) -> Response: + if not response.text: + return EmptyResponse(response.status_code) + try: + return JsonResponse(response) + except ValueError: + return PlainResponse(response) + + +class Response: + def __init__( + self, status_code: int, content: Any, headers: Mapping[str, str] + ) -> None: + self._status_code = status_code + self._content = content + self._headers = headers + + def content(self) -> Any: + if self._is_error(): + if self._status_code == 429: + reset_at = int(self._headers.get("x-ratelimit-reset", "-1")) + raise RateLimitError( + error_code=self._error_code(), + message=self._error_message(), + reset_at=reset_at, + headers=self._headers, + ) + if self._error_code() == "mfa_required": + raise Auth0Error( + status_code=self._status_code, + error_code=self._error_code(), + message=self._error_message(), + content=self._content, + headers=self._headers + ) + + raise Auth0Error( + status_code=self._status_code, + error_code=self._error_code(), + message=self._error_message(), + headers=self._headers + ) + else: + return self._content + + def _is_error(self) -> bool: + return self._status_code is None or self._status_code >= 400 + + # Adding these methods to force implementation in subclasses because they are references in this parent class + def _error_code(self): + raise NotImplementedError + + def _error_message(self): + raise NotImplementedError + + +class JsonResponse(Response): + def __init__(self, response: requests.Response | RequestsResponse) -> None: + content = loads(response.text) + super().__init__(response.status_code, content, response.headers) + + def _error_code(self) -> str: + if "errorCode" in self._content: + return self._content.get("errorCode") + elif "error" in self._content: + return self._content.get("error") + elif "code" in self._content: + return self._content.get("code") + else: + return UNKNOWN_ERROR + + def _error_message(self) -> str: + if "error_description" in self._content: + return self._content.get("error_description") + message = self._content.get("message", "") + if message is not None and message != "": + return message + return self._content.get("error", "") + + +class PlainResponse(Response): + def __init__(self, response: requests.Response | RequestsResponse) -> None: + super().__init__(response.status_code, response.text, response.headers) + + def _error_code(self) -> str: + return UNKNOWN_ERROR + + def _error_message(self) -> str: + return self._content + + +class EmptyResponse(Response): + def __init__(self, status_code: int) -> None: + super().__init__(status_code, "", {}) + + def _error_code(self) -> str: + return UNKNOWN_ERROR + + def _error_message(self) -> str: + return "" diff --git a/auth0/rest_async.py b/auth0/rest_async.py new file mode 100644 index 00000000..0c4e2851 --- /dev/null +++ b/auth0/rest_async.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +import aiohttp + +from auth0.exceptions import RateLimitError +from auth0.types import RequestData + +from .rest import EmptyResponse, JsonResponse, PlainResponse, Response, RestClient + + +def _clean_params(params: dict[Any, Any] | None) -> dict[Any, Any] | None: + if params is None: + return params + return {k: v for k, v in params.items() if v is not None} + + +class AsyncRestClient(RestClient): + """Provides simple methods for handling all RESTful api endpoints. + + Args: + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. Overrides matching + options passed to the constructor. + (defaults to 3) + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._session: aiohttp.ClientSession | None = None + sock_connect, sock_read = ( + self.timeout + if isinstance(self.timeout, tuple) + else (self.timeout, self.timeout) + ) + self.timeout = aiohttp.ClientTimeout( + sock_connect=sock_connect, sock_read=sock_read + ) # type: ignore[assignment] + + def set_session(self, session: aiohttp.ClientSession) -> None: + """Set Client Session to improve performance by reusing session. + Session should be closed manually or within context manager. + """ + self._session = session + + async def _request_with_session( + self, session: aiohttp.ClientSession, *args: Any, **kwargs: Any + ) -> Any: + # Track the API request attempt number + attempt = 0 + + # Reset the metrics tracker + self._metrics = {"retries": 0, "backoff": []} + + while True: + # Increment attempt number + attempt += 1 + + try: + async with session.request(*args, **kwargs) as response: + return await self._process_response(response) + + except RateLimitError as e: + # If the attempt number is greater than the configured retries, raise RateLimitError + if attempt > self._retries: + raise e + + wait = self._calculate_wait(attempt) + + # Skip calling sleep() when running unit tests + if self._skip_sleep is False: + # sleep() functions in seconds, so convert the milliseconds formula above accordingly + await asyncio.sleep(wait / 1000) + + async def _request(self, *args: Any, **kwargs: Any) -> Any: + kwargs["headers"] = kwargs.get("headers", self.base_headers) + kwargs["timeout"] = self.timeout + if self._session is not None: + # Request with re-usable session + return await self._request_with_session(self._session, *args, **kwargs) + else: + # Request without re-usable session + async with aiohttp.ClientSession() as session: + return await self._request_with_session(session, *args, **kwargs) + + async def get( + self, + url: str, + params: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + request_headers = self.base_headers.copy() + request_headers.update(headers or {}) + + return await self._request( + "get", url, params=_clean_params(params), headers=request_headers + ) + + async def post( + self, + url: str, + data: RequestData | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + request_headers = self.base_headers.copy() + request_headers.update(headers or {}) + return await self._request("post", url, json=data, headers=request_headers) + + async def file_post( + self, + url: str, + data: dict[str, Any], + files: dict[str, Any], + ) -> Any: + headers = self.base_headers.copy() + headers.pop("Content-Type") + return await self._request("post", url, data={**data, **files}, headers=headers) + + async def patch(self, url: str, data: RequestData | None = None) -> Any: + return await self._request("patch", url, json=data) + + async def put(self, url: str, data: RequestData | None = None) -> Any: + return await self._request("put", url, json=data) + + async def delete( + self, + url: str, + params: dict[str, Any] | None = None, + data: RequestData | None = None, + ) -> Any: + return await self._request( + "delete", url, json=data, params=_clean_params(params) or {} + ) + + async def _process_response(self, response: aiohttp.ClientResponse) -> Any: + parsed_response = await self._parse(response) + return parsed_response.content() + + async def _parse(self, response: aiohttp.ClientResponse) -> Response: + text = await response.text() + requests_response = RequestsResponse(response, text) + if not text: + return EmptyResponse(response.status) + try: + return JsonResponse(requests_response) + except ValueError: + return PlainResponse(requests_response) + + +class RequestsResponse: + def __init__(self, response: aiohttp.ClientResponse, text: str) -> None: + self.status_code = response.status + self.headers = response.headers + self.text = text diff --git a/auth0/v3/test/__init__.py b/auth0/test/__init__.py similarity index 100% rename from auth0/v3/test/__init__.py rename to auth0/test/__init__.py diff --git a/auth0/v3/test/authentication/__init__.py b/auth0/test/authentication/__init__.py similarity index 100% rename from auth0/v3/test/authentication/__init__.py rename to auth0/test/authentication/__init__.py diff --git a/auth0/test/authentication/test_back_channel_login.py b/auth0/test/authentication/test_back_channel_login.py new file mode 100644 index 00000000..2b8705a2 --- /dev/null +++ b/auth0/test/authentication/test_back_channel_login.py @@ -0,0 +1,210 @@ + +import unittest +from unittest import mock +import json + +import requests +from ...exceptions import Auth0Error, RateLimitError + +from ...authentication.back_channel_login import BackChannelLogin + +class TestBackChannelLogin(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_ciba(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + + g.back_channel_login( + binding_message="This is a binding message", + login_hint="{ \"format\": \"iss_sub\", \"iss\": \"https://my.domain.auth0.com/\", \"sub\": \"auth0|[USER ID]\" }", + scope="openid", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/bc-authorize") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "binding_message": "This is a binding message", + "login_hint": "{ \"format\": \"iss_sub\", \"iss\": \"https://my.domain.auth0.com/\", \"sub\": \"auth0|[USER ID]\" }", + "scope": "openid", + }, + ) + + @mock.patch("requests.request") + def test_server_error(self, mock_requests_request): + response = requests.Response() + response.status_code = 400 + response._content = b'{"error":"foo"}' + mock_requests_request.return_value = response + + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + with self.assertRaises(Auth0Error) as context: + g.back_channel_login( + binding_message="msg", + login_hint="hint", + scope="openid" + ) + self.assertEqual(context.exception.status_code, 400) + self.assertEqual(context.exception.message, 'foo') + + @mock.patch("auth0.rest.RestClient.post") + def test_should_require_binding_message(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + + # Expecting an exception to be raised when binding_message is missing + with self.assertRaises(Exception) as context: + g.back_channel_login( + login_hint='{ "format": "iss_sub", "iss": "https://my.domain.auth0.com/", "sub": "auth0|USER_ID" }', + scope="openid", + ) + + # Assert the error message is correct + self.assertIn("missing 1 required positional argument: \'binding_message\'", str(context.exception)) + + @mock.patch("auth0.rest.RestClient.post") + def test_should_require_login_hint(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + + # Expecting an exception to be raised when login_hint is missing + with self.assertRaises(Exception) as context: + g.back_channel_login( + binding_message="This is a binding message.", + scope="openid", + ) + + # Assert the error message is correct + self.assertIn("missing 1 required positional argument: \'login_hint\'", str(context.exception)) + + @mock.patch("auth0.rest.RestClient.post") + def test_should_require_scope(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + + # Expecting an exception to be raised when scope is missing + with self.assertRaises(Exception) as context: + g.back_channel_login( + binding_message="This is a binding message.", + login_hint='{ "format": "iss_sub", "iss": "https://my.domain.auth0.com/", "sub": "auth0|USER_ID" }', + ) + + # Assert the error message is correct + self.assertIn("missing 1 required positional argument: \'scope\'", str(context.exception)) + + @mock.patch("auth0.rest.RestClient.post") + def test_with_authorization_details(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + g.back_channel_login( + binding_message="This is a binding message.", + login_hint= json.dumps({"format": "iss_sub", "iss": "https://my.domain.auth0.com/", "sub": "auth0|USER_ID"}), + scope="openid", + authorization_details=[ + { + "type":"payment_initiation","locations":["https://example.com/payments"], + "instructedAmount": + { + "currency":"EUR","amount":"123.50" + }, + "creditorName":"Merchant A", + "creditorAccount": + { + "bic":"ABCIDEFFXXX", + "iban":"DE021001001093071118603" + }, + "remittanceInformationUnstructured":"Ref Number Merchant" + } + ], + ) + + args, kwargs = mock_post.call_args + + expected_data = { + "client_id": "cid", + "client_secret": "clsec", + "binding_message": "This is a binding message.", + "login_hint": json.dumps({"format": "iss_sub", "iss": "https://my.domain.auth0.com/", "sub": "auth0|USER_ID"}), + "scope": "openid", + "authorization_details": json.dumps([ + { + "type":"payment_initiation","locations":["https://example.com/payments"], + "instructedAmount": + { + "currency":"EUR","amount":"123.50" + }, + "creditorName":"Merchant A", + "creditorAccount": + { + "bic":"ABCIDEFFXXX", + "iban":"DE021001001093071118603" + }, + "remittanceInformationUnstructured":"Ref Number Merchant" + } + ]), + } + + actual_data = kwargs["data"] + + self.assertEqual(args[0], "https://my.domain.com/bc-authorize") + + self.assertEqual( + actual_data, + expected_data, + "Request data does not match expected data after JSON serialization." + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_with_request_expiry(self, mock_post): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + + g.back_channel_login( + binding_message="This is a binding message", + login_hint="{ \"format\": \"iss_sub\", \"iss\": \"https://my.domain.auth0.com/\", \"sub\": \"auth0|[USER ID]\" }", + scope="openid", + requested_expiry=100 + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/bc-authorize") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "binding_message": "This is a binding message", + "login_hint": "{ \"format\": \"iss_sub\", \"iss\": \"https://my.domain.auth0.com/\", \"sub\": \"auth0|[USER ID]\" }", + "scope": "openid", + "requested_expiry": "100", + }, + ) + + def test_requested_expiry_negative_raises(self): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + with self.assertRaises(ValueError): + g.back_channel_login( + binding_message="msg", + login_hint="hint", + scope="openid", + requested_expiry=-10 + ) + + def test_requested_expiry_zero_raises(self): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + with self.assertRaises(ValueError): + g.back_channel_login( + binding_message="msg", + login_hint="hint", + scope="openid", + requested_expiry=0 + ) + + def test_requested_non_int_raises(self): + g = BackChannelLogin("my.domain.com", "cid", client_secret="clsec") + with self.assertRaises(ValueError): + g.back_channel_login( + binding_message="msg", + login_hint="hint", + scope="openid", + requested_expiry="string_instead_of_int" + ) diff --git a/auth0/test/authentication/test_base.py b/auth0/test/authentication/test_base.py new file mode 100644 index 00000000..a4f52d83 --- /dev/null +++ b/auth0/test/authentication/test_base.py @@ -0,0 +1,323 @@ +import base64 +import json +import sys +import unittest +from unittest import mock + +import requests + +from ...authentication.base import AuthenticationBase +from ...exceptions import Auth0Error, RateLimitError + + +class TestBase(unittest.TestCase): + def test_telemetry_enabled_by_default(self): + ab = AuthenticationBase("auth0.com", "cid") + base_headers = ab.client.base_headers + + user_agent = base_headers["User-Agent"] + auth0_client_bytes = base64.b64decode(base_headers["Auth0-Client"]) + auth0_client_json = auth0_client_bytes.decode("utf-8") + auth0_client = json.loads(auth0_client_json) + content_type = base_headers["Content-Type"] + + from auth0 import __version__ as auth0_version + + python_version = "{}.{}.{}".format( + sys.version_info.major, sys.version_info.minor, sys.version_info.micro + ) + + client_info = { + "name": "auth0-python", + "version": auth0_version, + "env": {"python": python_version}, + } + + self.assertEqual(user_agent, f"Python/{python_version}") + self.assertEqual(auth0_client, client_info) + self.assertEqual(content_type, "application/json") + + def test_telemetry_disabled(self): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + self.assertEqual(ab.client.base_headers, {"Content-Type": "application/json"}) + + @mock.patch("requests.request") + def test_post(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False, timeout=(10, 2)) + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + data = ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + mock_request.assert_called_with( + "POST", + "the-url", + json={"a": "b"}, + headers={"c": "d", "Content-Type": "application/json"}, + timeout=(10, 2), + ) + + self.assertEqual(data, {"x": "y"}) + + @mock.patch("requests.request") + def test_post_with_defaults(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + # Only required params are passed + data = ab.post("the-url") + + mock_request.assert_called_with( + "POST", + "the-url", + headers={"Content-Type": "application/json"}, + timeout=5.0, + ) + + self.assertEqual(data, {"x": "y"}) + + @mock.patch("requests.request") + def test_post_includes_telemetry(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid") + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + data = ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(mock_request.call_count, 1) + call_args, call_kwargs = mock_request.call_args + self.assertEqual(call_args[0], "POST") + self.assertEqual(call_args[1], "the-url") + self.assertEqual(call_kwargs["json"], {"a": "b"}) + headers = call_kwargs["headers"] + self.assertEqual(headers["c"], "d") + self.assertEqual(headers["Content-Type"], "application/json") + self.assertIn("User-Agent", headers) + self.assertIn("Auth0-Client", headers) + + self.assertEqual(data, {"x": "y"}) + + @mock.patch("requests.request") + def test_post_error(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = ( + '{"error": "e0","error_description": "desc"}' + ) + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "e0") + self.assertEqual(context.exception.message, "desc") + + @mock.patch("requests.request") + def test_post_error_mfa_required(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + mock_request.return_value.status_code = 403 + mock_request.return_value.text = '{"error": "mfa_required", "error_description": "Multifactor authentication required", "mfa_token": "Fe26...Ha"}' + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, 403) + self.assertEqual(context.exception.error_code, "mfa_required") + self.assertEqual( + context.exception.message, "Multifactor authentication required" + ) + self.assertEqual(context.exception.content.get("mfa_token"), "Fe26...Ha") + + @mock.patch("requests.request") + def test_post_rate_limit_error(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 429, "error": "e0", "error_description": "desc"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "e0") + self.assertEqual(context.exception.message, "desc") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + self.assertIsNotNone(context.exception.headers) + self.assertEqual(context.exception.headers["x-ratelimit-limit"], "3") + self.assertEqual(context.exception.headers["x-ratelimit-remaining"], "6") + self.assertEqual(context.exception.headers["x-ratelimit-reset"], "9") + + @mock.patch("requests.request") + def test_post_rate_limit_error_without_headers(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 429, "error": "e0", "error_description": "desc"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = {} + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "e0") + self.assertEqual(context.exception.message, "desc") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, -1) + self.assertIsNotNone(context.exception.headers) + self.assertEqual(context.exception.headers, {}) + + @mock.patch("requests.request") + def test_post_error_with_code_property(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = ( + '{"code": "e0","error_description": "desc"}' + ) + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "e0") + self.assertEqual(context.exception.message, "desc") + + @mock.patch("requests.request") + def test_post_error_with_no_error_code(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = '{"error_description": "desc"}' + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual(context.exception.message, "desc") + + @mock.patch("requests.request") + def test_post_error_with_text_response(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = "there has been a terrible error" + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual( + context.exception.message, "there has been a terrible error" + ) + + @mock.patch("requests.request") + def test_post_error_with_no_response_text(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = None + + with self.assertRaises(Auth0Error) as context: + ab.post("the-url", data={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual(context.exception.message, "") + + @mock.patch("requests.request") + def test_get(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False, timeout=(10, 2)) + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + data = ab.get("the-url", params={"a": "b"}, headers={"c": "d"}) + + mock_request.assert_called_with( + "GET", + "the-url", + params={"a": "b"}, + headers={"c": "d", "Content-Type": "application/json"}, + timeout=(10, 2), + ) + + self.assertEqual(data, {"x": "y"}) + + @mock.patch("requests.request") + def test_get_with_defaults(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid", telemetry=False) + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + # Only required params are passed + data = ab.get("the-url") + + mock_request.assert_called_with( + "GET", + "the-url", + headers={"Content-Type": "application/json"}, + timeout=5.0, + ) + + self.assertEqual(data, {"x": "y"}) + + @mock.patch("requests.request") + def test_get_includes_telemetry(self, mock_request): + ab = AuthenticationBase("auth0.com", "cid") + + mock_request.return_value.status_code = 200 + mock_request.return_value.text = '{"x": "y"}' + + data = ab.get("the-url", params={"a": "b"}, headers={"c": "d"}) + + self.assertEqual(mock_request.call_count, 1) + call_args, call_kwargs = mock_request.call_args + self.assertEqual(call_args[0], "GET") + self.assertEqual(call_args[1], "the-url") + self.assertEqual(call_kwargs["params"], {"a": "b"}) + headers = call_kwargs["headers"] + self.assertEqual(headers["c"], "d") + self.assertEqual(headers["Content-Type"], "application/json") + self.assertIn("User-Agent", headers) + self.assertIn("Auth0-Client", headers) + + self.assertEqual(data, {"x": "y"}) + + # TODO: Replace the following with more reliable tests. Failing on GitHub Actions. + + # def test_get_can_timeout(self): + # ab = AuthenticationBase("auth0.com", "cid", timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # ab.get("https://google.com", params={"a": "b"}, headers={"c": "d"}) + + # def test_post_can_timeout(self): + # ab = AuthenticationBase("auth0.com", "cid", timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # ab.post("https://google.com", data={"a": "b"}, headers={"c": "d"}) diff --git a/auth0/test/authentication/test_database.py b/auth0/test/authentication/test_database.py new file mode 100644 index 00000000..1572e1ae --- /dev/null +++ b/auth0/test/authentication/test_database.py @@ -0,0 +1,102 @@ +import unittest +from unittest import mock + +from ...authentication.database import Database + + +class TestDatabase(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_signup(self, mock_post): + d = Database("my.domain.com", "cid") + + # using only email and password + d.signup(email="a@b.com", password="pswd", connection="conn") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/dbconnections/signup") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "password": "pswd", + "connection": "conn", + }, + ) + + # Using also optional properties + sample_meta = {"hobby": "surfing", "preference": {"color": "pink"}} + d.signup( + email="a@b.com", + password="pswd", + connection="conn", + username="usr", + user_metadata=sample_meta, + given_name="john", + family_name="doe", + name="john doe", + nickname="johnny", + picture="avatars.com/john-doe", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/dbconnections/signup") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "password": "pswd", + "connection": "conn", + "username": "usr", + "user_metadata": sample_meta, + "given_name": "john", + "family_name": "doe", + "name": "john doe", + "nickname": "johnny", + "picture": "avatars.com/john-doe", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_change_password(self, mock_post): + d = Database("my.domain.com", "cid") + + # ignores the password argument + d.change_password(email="a@b.com", password="pswd", connection="conn") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/dbconnections/change_password") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "connection": "conn", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_change_password_with_organization_param(self, mock_post): + d = Database("my.domain.com", "cid") + + # ignores the password argument + d.change_password( + email="a@b.com", password="pswd", connection="conn", organization="org_id" + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/dbconnections/change_password") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "connection": "conn", + "organization": "org_id", + }, + ) diff --git a/auth0/test/authentication/test_delegated.py b/auth0/test/authentication/test_delegated.py new file mode 100644 index 00000000..0ad817f2 --- /dev/null +++ b/auth0/test/authentication/test_delegated.py @@ -0,0 +1,72 @@ +import unittest +from unittest import mock + +from ...authentication.delegated import Delegated + + +class TestDelegated(unittest.TestCase): + @mock.patch("auth0.authentication.delegated.Delegated.post") + def test_get_token_id_token(self, mock_post): + d = Delegated("my.domain.com", "cid") + + d.get_token( + target="tgt", + api_type="apt", + grant_type="gt", + id_token="idt", + scope="openid profile", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/delegation") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "grant_type": "gt", + "id_token": "idt", + "target": "tgt", + "scope": "openid profile", + "api_type": "apt", + }, + ) + + @mock.patch("auth0.authentication.delegated.Delegated.post") + def test_get_token_refresh_token(self, mock_post): + d = Delegated("my.domain.com", "cid") + + d.get_token( + target="tgt", + api_type="apt", + grant_type="gt", + refresh_token="rtk", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/delegation") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "grant_type": "gt", + "refresh_token": "rtk", + "target": "tgt", + "scope": "openid", + "api_type": "apt", + }, + ) + + @mock.patch("auth0.authentication.delegated.Delegated.post") + def test_get_token_value_error(self, mock_post): + d = Delegated("my.domain.com", "cid") + + with self.assertRaises(ValueError): + d.get_token( + target="tgt", + api_type="apt", + grant_type="gt", + refresh_token="rtk", + id_token="idt", + ) diff --git a/auth0/test/authentication/test_enterprise.py b/auth0/test/authentication/test_enterprise.py new file mode 100644 index 00000000..0021f0ac --- /dev/null +++ b/auth0/test/authentication/test_enterprise.py @@ -0,0 +1,27 @@ +import unittest +from unittest import mock + +from ...authentication.enterprise import Enterprise + + +class TestEnterprise(unittest.TestCase): + @mock.patch("auth0.authentication.enterprise.Enterprise.get") + def test_saml_metadata(self, mock_get): + e = Enterprise("my.domain.com", "cid") + + e.saml_metadata() + + mock_get.assert_called_with(url="https://my.domain.com/samlp/metadata/cid") + + @mock.patch("auth0.authentication.enterprise.Enterprise.get") + def test_wsfed_metadata(self, mock_get): + e = Enterprise("my.domain.com", "cid") + + e.wsfed_metadata() + + mock_get.assert_called_with( + url=( + "https://my.domain.com/wsfed/FederationMetadata" + "/2007-06/FederationMetadata.xml" + ) + ) diff --git a/auth0/test/authentication/test_get_token.py b/auth0/test/authentication/test_get_token.py new file mode 100644 index 00000000..7c98d341 --- /dev/null +++ b/auth0/test/authentication/test_get_token.py @@ -0,0 +1,416 @@ +import unittest +import requests +from fnmatch import fnmatch +from unittest import mock +from unittest.mock import ANY + +from cryptography.hazmat.primitives import asymmetric, serialization + +from ...exceptions import RateLimitError +from ...authentication.get_token import GetToken + + +def get_private_key(): + private_key = asymmetric.rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + return private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + +class TestGetToken(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_authorization_code(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.authorization_code( + code="cd", + grant_type="gt", + redirect_uri="idt", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "code": "cd", + "grant_type": "gt", + "redirect_uri": "idt", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_authorization_code_with_client_assertion(self, mock_post): + g = GetToken( + "my.domain.com", "cid", client_assertion_signing_key=get_private_key() + ) + + g.authorization_code(code="cd", grant_type="gt", redirect_uri="idt") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_assertion": ANY, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "code": "cd", + "grant_type": "gt", + "redirect_uri": "idt", + }, + ) + + self.assertTrue(fnmatch(kwargs["data"]["client_assertion"], "*.*.*")) + + @mock.patch("auth0.rest.RestClient.post") + def test_authorization_code_pkce(self, mock_post): + g = GetToken("my.domain.com", "cid") + + g.authorization_code_pkce( + code_verifier="cdver", + code="cd", + grant_type="gt", + redirect_uri="idt", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "code_verifier": "cdver", + "code": "cd", + "grant_type": "gt", + "redirect_uri": "idt", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_client_credentials(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.client_credentials(audience="aud", grant_type="gt") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "audience": "aud", + "grant_type": "gt", + "organization": None, + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_client_credentials_with_client_assertion(self, mock_post): + g = GetToken( + "my.domain.com", "cid", client_assertion_signing_key=get_private_key() + ) + + g.client_credentials("aud", grant_type="gt") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_assertion": ANY, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "audience": "aud", + "grant_type": "gt", + "organization": None, + }, + ) + + self.assertTrue(fnmatch(kwargs["data"]["client_assertion"], "*.*.*")) + + @mock.patch("auth0.rest.RestClient.post") + def test_client_credentials_with_organization(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.client_credentials("aud", organization="my-org") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "grant_type": "client_credentials", + "client_secret": "clsec", + "audience": "aud", + "organization": "my-org", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_login(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.login( + username="usrnm", + password="pswd", + scope="http://test.com/api", + realm="rlm", + audience="aud", + grant_type="gt", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "username": "usrnm", + "password": "pswd", + "scope": "http://test.com/api", + "realm": "rlm", + "audience": "aud", + "grant_type": "gt", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_login_simple(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.login( + username="usrnm", + password="pswd", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "username": "usrnm", + "password": "pswd", + "realm": None, + "scope": None, + "audience": None, + "grant_type": "http://auth0.com/oauth/grant-type/password-realm", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_login_with_forwarded_for(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.login(username="usrnm", password="pswd", forwarded_for="192.168.0.1") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["headers"], + { + "auth0-forwarded-for": "192.168.0.1", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_refresh_token(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.refresh_token( + refresh_token="rt", + grant_type="gt", + scope="s", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "clsec", + "refresh_token": "rt", + "grant_type": "gt", + "scope": "s", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_passwordless_login_with_sms(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="csec") + + g.passwordless_login( + username="123456", + otp="abcd", + realm="sms", + audience="aud", + scope="openid", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "csec", + "realm": "sms", + "grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp", + "username": "123456", + "otp": "abcd", + "audience": "aud", + "scope": "openid", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_passwordless_login_with_email(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="csec") + + g.passwordless_login( + username="a@b.c", + otp="abcd", + realm="email", + audience="aud", + scope="openid", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "csec", + "realm": "email", + "grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp", + "username": "a@b.c", + "otp": "abcd", + "audience": "aud", + "scope": "openid", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_backchannel_login(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="csec") + + g.backchannel_login( + auth_req_id="reqid", + grant_type="urn:openid:params:grant-type:ciba", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "csec", + "auth_req_id": "reqid", + "grant_type": "urn:openid:params:grant-type:ciba", + }, + ) + + @mock.patch("requests.request") + def test_backchannel_login_headers_on_slow_down(self, mock_requests_request): + response = requests.Response() + response.status_code = 429 + response.headers = {"Retry-After": "100"} + response._content = b'{"error":"slow_down"}' + mock_requests_request.return_value = response + + g = GetToken("my.domain.com", "cid", client_secret="csec") + + with self.assertRaises(RateLimitError) as context: + g.backchannel_login( + auth_req_id="reqid", + grant_type="urn:openid:params:grant-type:ciba", + ) + self.assertEqual(context.exception.headers["Retry-After"], "100") + self.assertEqual(context.exception.status_code, 429) + + @mock.patch("auth0.rest.RestClient.post") + def test_connection_login(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="csec") + + g.access_token_for_connection( + grant_type="urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token", + subject_token_type="urn:ietf:params:oauth:token-type:refresh_token", + subject_token="refid", + requested_token_type="http://auth0.com/oauth/token-type/federated-connection-access-token", + connection="google-oauth2" + ) + + args, kwargs = mock_post.call_args + + print(kwargs["data"]) + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token", + "client_id": "cid", + "client_secret": "csec", + "subject_token_type": "urn:ietf:params:oauth:token-type:refresh_token", + "subject_token": "refid", + "requested_token_type": "http://auth0.com/oauth/token-type/federated-connection-access-token", + "connection": "google-oauth2" + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_connection_login_with_login_hint(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="csec") + + g.access_token_for_connection( + subject_token_type="urn:ietf:params:oauth:token-type:refresh_token", + subject_token="refid", + requested_token_type="http://auth0.com/oauth/token-type/federated-connection-access-token", + connection="google-oauth2", + login_hint="john.doe@example.com" + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token", + "client_id": "cid", + "client_secret": "csec", + "subject_token_type": "urn:ietf:params:oauth:token-type:refresh_token", + "subject_token": "refid", + "requested_token_type": "http://auth0.com/oauth/token-type/federated-connection-access-token", + "connection": "google-oauth2", + "login_hint": "john.doe@example.com" + }, + ) \ No newline at end of file diff --git a/auth0/test/authentication/test_passwordless.py b/auth0/test/authentication/test_passwordless.py new file mode 100644 index 00000000..e726ecc9 --- /dev/null +++ b/auth0/test/authentication/test_passwordless.py @@ -0,0 +1,102 @@ +import unittest +from unittest import mock + +from ...authentication.passwordless import Passwordless + + +class TestPasswordless(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_send_email(self, mock_post): + p = Passwordless("my.domain.com", "cid") + + p.email(email="a@b.com", send="snd") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/passwordless/start") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "send": "snd", + "connection": "email", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_send_email_with_auth_params(self, mock_post): + p = Passwordless("my.domain.com", "cid") + + p.email(email="a@b.com", send="snd", auth_params={"a": "b"}) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/passwordless/start") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "email": "a@b.com", + "send": "snd", + "authParams": {"a": "b"}, + "connection": "email", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_send_email_with_client_secret(self, mock_post): + p = Passwordless("my.domain.com", "cid", client_secret="csecret") + + p.email(email="a@b.com", send="snd") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/passwordless/start") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "csecret", + "email": "a@b.com", + "send": "snd", + "connection": "email", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_send_sms(self, mock_post): + p = Passwordless("my.domain.com", "cid") + + p.sms(phone_number="123456") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/passwordless/start") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "phone_number": "123456", + "connection": "sms", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_send_sms_with_client_secret(self, mock_post): + p = Passwordless("my.domain.com", "cid", client_secret="csecret") + + p.sms(phone_number="123456") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/passwordless/start") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "csecret", + "phone_number": "123456", + "connection": "sms", + }, + ) diff --git a/auth0/test/authentication/test_pushed_authorization_requests.py b/auth0/test/authentication/test_pushed_authorization_requests.py new file mode 100644 index 00000000..6bcb3ca7 --- /dev/null +++ b/auth0/test/authentication/test_pushed_authorization_requests.py @@ -0,0 +1,104 @@ +import unittest +import json +from unittest import mock + +from ...authentication.pushed_authorization_requests import PushedAuthorizationRequests + + +class TestRevokeToken(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_par(self, mock_post): + a = PushedAuthorizationRequests("my.domain.com", "cid", client_secret="sh!") + a.pushed_authorization_request( + response_type="code", redirect_uri="https://example.com/callback" + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/par") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "sh!", + "response_type": "code", + "redirect_uri": "https://example.com/callback", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_par_custom_params(self, mock_post): + a = PushedAuthorizationRequests("my.domain.com", "cid", client_secret="sh!") + a.pushed_authorization_request( + response_type="code", redirect_uri="https://example.com/callback", foo="bar" + ) + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/par") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "client_secret": "sh!", + "response_type": "code", + "redirect_uri": "https://example.com/callback", + "foo": "bar", + }, + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_with_authorization_details(self, mock_post): + a = PushedAuthorizationRequests("my.domain.com", "cid", client_secret="sh!") + a.pushed_authorization_request( + response_type="code", + redirect_uri="https://example.com/callback", + authorization_details=[{"type": "money_transfer", "instructedAmount": {"amount": 2500, "currency": "USD"}}], + ) + + args, kwargs = mock_post.call_args + + expected_data = { + "client_id": "cid", + "client_secret": "sh!", + "response_type": "code", + "redirect_uri": "https://example.com/callback", + "authorization_details": [{"type": "money_transfer", "instructedAmount": {"amount": 2500, "currency": "USD"}}], + } + + actual_data = kwargs["data"] + + self.assertEqual(args[0], "https://my.domain.com/oauth/par") + + self.assertEqual( + json.dumps(actual_data, sort_keys=True), + json.dumps(expected_data, sort_keys=True) + ) + + @mock.patch("auth0.rest.RestClient.post") + def test_jar(self, mock_post): + a = PushedAuthorizationRequests("my.domain.com", "cid", client_secret="sh!") + a.pushed_authorization_request( + response_type="code", + redirect_uri="https://example.com/callback", + request="my-jwt-request", + ) + + args, kwargs = mock_post.call_args + + expected_data = { + "client_id": "cid", + "client_secret": "sh!", + "response_type": "code", + "redirect_uri": "https://example.com/callback", + "request": 'my-jwt-request', + } + + actual_data = kwargs["data"] + + self.assertEqual(args[0], "https://my.domain.com/oauth/par") + + self.assertEqual( + json.dumps(actual_data, sort_keys=True), + json.dumps(expected_data, sort_keys=True) + ) \ No newline at end of file diff --git a/auth0/test/authentication/test_revoke_token.py b/auth0/test/authentication/test_revoke_token.py new file mode 100644 index 00000000..2f46c899 --- /dev/null +++ b/auth0/test/authentication/test_revoke_token.py @@ -0,0 +1,29 @@ +import unittest +from unittest import mock + +from ...authentication.revoke_token import RevokeToken + + +class TestRevokeToken(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.post") + def test_revoke_refresh_token(self, mock_post): + a = RevokeToken("my.domain.com", "cid") + + # regular apps + a.revoke_refresh_token(token="tkn") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/revoke") + self.assertEqual(kwargs["data"], {"client_id": "cid", "token": "tkn"}) + + # confidential apps + a = RevokeToken("my.domain.com", "cid", client_secret="sh!") + a.revoke_refresh_token(token="tkn") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/revoke") + self.assertEqual( + kwargs["data"], {"client_id": "cid", "token": "tkn", "client_secret": "sh!"} + ) diff --git a/auth0/test/authentication/test_social.py b/auth0/test/authentication/test_social.py new file mode 100644 index 00000000..8e99fc3c --- /dev/null +++ b/auth0/test/authentication/test_social.py @@ -0,0 +1,46 @@ +import unittest +from unittest import mock + +from ...authentication.social import Social + + +class TestSocial(unittest.TestCase): + @mock.patch("auth0.authentication.social.Social.post") + def test_login(self, mock_post): + s = Social("a.b.c", "cid") + s.login(access_token="atk", connection="conn") + + args, kwargs = mock_post.call_args + + self.assertEqual("https://a.b.c/oauth/access_token", args[0]) + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "access_token": "atk", + "connection": "conn", + "scope": "openid", + }, + ) + + @mock.patch("auth0.authentication.social.Social.post") + def test_login_with_scope(self, mock_post): + s = Social("a.b.c", "cid") + s.login( + access_token="atk", + connection="conn", + scope="openid profile", + ) + + args, kwargs = mock_post.call_args + + self.assertEqual("https://a.b.c/oauth/access_token", args[0]) + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "access_token": "atk", + "connection": "conn", + "scope": "openid profile", + }, + ) diff --git a/auth0/v3/test/authentication/test_token_verifier.py b/auth0/test/authentication/test_token_verifier.py similarity index 66% rename from auth0/v3/test/authentication/test_token_verifier.py rename to auth0/test/authentication/test_token_verifier.py index d1306d3d..33dab693 100644 --- a/auth0/v3/test/authentication/test_token_verifier.py +++ b/auth0/test/authentication/test_token_verifier.py @@ -1,19 +1,35 @@ import json - -import jwt -import unittest import time +import unittest +from unittest.mock import MagicMock, patch -from mock import MagicMock, patch +import jwt -from ...authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier, \ - SymmetricSignatureVerifier, JwksFetcher, SignatureVerifier +from ...authentication.token_verifier import ( + AsymmetricSignatureVerifier, + JwksFetcher, + SignatureVerifier, + SymmetricSignatureVerifier, + TokenVerifier, +) from ...exceptions import TokenValidationError RSA_PUB_KEY_1_PEM = b"""-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4\nyCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9\n83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs\nWXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT\n69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8\nAziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0\nYwIDAQAB\n-----END PUBLIC KEY-----\n""" RSA_PUB_KEY_2_PEM = b"""-----BEGIN PUBLIC KEY-----\nMDowDQYJKoZIhvcNAQEBBQADKQAwJgIfAI7TBUCn8e1hj/fNpb5dqMf8Jj6Ja6qN\npqyeOGYEzAIDAQAB\n-----END PUBLIC KEY-----\n""" -RSA_PUB_KEY_1_JWK = {"kty": "RSA", "use": "sig", "n": "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", "e": "AQAB", "kid": "test-key-1"} -RSA_PUB_KEY_2_JWK = {"kty": "RSA", "use": "sig", "n": "jtMFQKfx7WGP982lvl2ox_wmPolrqo2mrJ44ZgTM", "e": "AQAB", "kid": "test-key-2"} +RSA_PUB_KEY_1_JWK = { + "kty": "RSA", + "use": "sig", + "n": "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", + "e": "AQAB", + "kid": "test-key-1", +} +RSA_PUB_KEY_2_JWK = { + "kty": "RSA", + "use": "sig", + "n": "jtMFQKfx7WGP982lvl2ox_wmPolrqo2mrJ44ZgTM", + "e": "AQAB", + "kid": "test-key-2", +} JWKS_RESPONSE_SINGLE_KEY = {"keys": [RSA_PUB_KEY_1_JWK]} JWKS_RESPONSE_MULTIPLE_KEYS = {"keys": [RSA_PUB_KEY_1_JWK, RSA_PUB_KEY_2_JWK]} HMAC_SHARED_SECRET = "secret" @@ -22,15 +38,14 @@ DEFAULT_LEEWAY = 60 expectations = { - "audience": "tokens-test-123", - "audience_alt": "external-test-999", - "issuer": "https://tokens-test.auth0.com/", - "nonce": "a1b2c3d4e5" + "audience": "tokens-test-123", + "audience_alt": "external-test-999", + "issuer": "https://tokens-test.auth0.com/", + "nonce": "a1b2c3d4e5", } class TestSignatureVerifier(unittest.TestCase): - def test_fail_at_creation_with_invalid_algorithm(self): alg = 12345 @@ -54,6 +69,14 @@ def test_asymmetric_verifier_uses_rs256_alg(self): verifier = AsymmetricSignatureVerifier("some URL") self.assertEqual(verifier._algorithm, "RS256") + def test_asymmetric_verifier_uses_default_jwks_cache_ttl(self): + verifier = AsymmetricSignatureVerifier("some URL") + self.assertEqual(verifier._fetcher._cache_ttl, JwksFetcher.CACHE_TTL) + + def test_asymmetric_verifier_uses_provided_jwks_cache_ttl(self): + verifier = AsymmetricSignatureVerifier("some URL", cache_ttl=3600) + self.assertEqual(verifier._fetcher._cache_ttl, 3600) + def test_symmetric_verifier_fetches_key(self): verifier = SymmetricSignatureVerifier("some secret") key = verifier._fetch_key() @@ -62,18 +85,17 @@ def test_symmetric_verifier_fetches_key(self): self.assertEqual(key, "some secret") def test_asymmetric_verifier_fetches_key(self): - - mock_fetcher = JwksFetcher('some URL') - mock_fetcher.get_key = MagicMock('get_key') + mock_fetcher = JwksFetcher("some URL") + mock_fetcher.get_key = MagicMock("get_key") mock_fetcher.get_key.return_value = RSA_PUB_KEY_1_JWK verifier = AsymmetricSignatureVerifier("some URL") verifier._fetcher = mock_fetcher - key = verifier._fetch_key('test-key') + key = verifier._fetch_key("test-key") args, kwargs = mock_fetcher.get_key.call_args - self.assertEqual(args[0], 'test-key') + self.assertEqual(args[0], "test-key") self.assertEqual(mock_fetcher, verifier._fetcher) self.assertEqual(mock_fetcher._jwks_url, "some URL") @@ -86,34 +108,43 @@ def test_fails_with_none_algorithm(self): verifier = SymmetricSignatureVerifier("some secret") with self.assertRaises(Exception) as err: verifier.verify_signature(jwt) - self.assertEqual(str(err.exception), 'Signature algorithm of "none" is not supported. Expected the token to be signed with "HS256"') + self.assertEqual( + str(err.exception), + 'Signature algorithm of "none" is not supported. Expected the token to be' + ' signed with "HS256"', + ) verifier = AsymmetricSignatureVerifier("some url") with self.assertRaises(Exception) as err: verifier.verify_signature(jwt) - self.assertEqual(str(err.exception), - 'Signature algorithm of "none" is not supported. Expected the token to be signed with "RS256"') + self.assertEqual( + str(err.exception), + 'Signature algorithm of "none" is not supported. Expected the token to be' + ' signed with "RS256"', + ) class TestJwksFetcher(unittest.TestCase): - @staticmethod def _get_pem_bytes(rsa_public_key): # noinspection PyPackageRequirements # requirement already includes cryptography -> pyjwt[crypto] from cryptography.hazmat.primitives import serialization - return rsa_public_key.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) - @patch('requests.get') + return rsa_public_key.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + + @patch("requests.get") def test_get_jwks_json_twice_on_cache_expired(self, mock_get): - JWKS_URL = 'https://app.myhosting.com/.well-known/jwks.json' + JWKS_URL = "https://app.myhosting.com/.well-known/jwks.json" fetcher = JwksFetcher(JWKS_URL, cache_ttl=1) mock_get.return_value.ok = True mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = JWKS_RESPONSE_SINGLE_KEY - key_1 = fetcher.get_key('test-key-1') + key_1 = fetcher.get_key("test-key-1") expected_key_1_pem = self._get_pem_bytes(key_1) self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) @@ -123,24 +154,24 @@ def test_get_jwks_json_twice_on_cache_expired(self, mock_get): time.sleep(2) # 2 seconds has passed, cache should be expired - key_1 = fetcher.get_key('test-key-1') + key_1 = fetcher.get_key("test-key-1") expected_key_1_pem = self._get_pem_bytes(key_1) self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) mock_get.assert_called_with(JWKS_URL) self.assertEqual(mock_get.call_count, 2) - @patch('requests.get') + @patch("requests.get") def test_get_jwks_json_once_on_cache_hit(self, mock_get): - JWKS_URL = 'https://app.myhosting.com/.well-known/jwks.json' + JWKS_URL = "https://app.myhosting.com/.well-known/jwks.json" fetcher = JwksFetcher(JWKS_URL) mock_get.return_value.ok = True mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = JWKS_RESPONSE_MULTIPLE_KEYS - key_1 = fetcher.get_key('test-key-1') - key_2 = fetcher.get_key('test-key-2') + key_1 = fetcher.get_key("test-key-1") + key_2 = fetcher.get_key("test-key-2") expected_key_1_pem = self._get_pem_bytes(key_1) expected_key_2_pem = self._get_pem_bytes(key_2) self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) @@ -149,17 +180,17 @@ def test_get_jwks_json_once_on_cache_hit(self, mock_get): mock_get.assert_called_with(JWKS_URL) self.assertEqual(mock_get.call_count, 1) - @patch('requests.get') + @patch("requests.get") def test_fetches_jwks_json_forced_on_cache_miss(self, mock_get): - JWKS_URL = 'https://app.myhosting.com/.well-known/jwks.json' + JWKS_URL = "https://app.myhosting.com/.well-known/jwks.json" fetcher = JwksFetcher(JWKS_URL) mock_get.return_value.ok = True mock_get.return_value.status_code = 200 - mock_get.return_value.json.return_value = {'keys':[RSA_PUB_KEY_1_JWK]} + mock_get.return_value.json.return_value = {"keys": [RSA_PUB_KEY_1_JWK]} # Triggers the first call - key_1 = fetcher.get_key('test-key-1') + key_1 = fetcher.get_key("test-key-1") expected_key_1_pem = self._get_pem_bytes(key_1) self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) @@ -169,16 +200,16 @@ def test_fetches_jwks_json_forced_on_cache_miss(self, mock_get): mock_get.return_value.json.return_value = JWKS_RESPONSE_MULTIPLE_KEYS # Triggers the second call - key_2 = fetcher.get_key('test-key-2') + key_2 = fetcher.get_key("test-key-2") expected_key_2_pem = self._get_pem_bytes(key_2) self.assertEqual(expected_key_2_pem, RSA_PUB_KEY_2_PEM) mock_get.assert_called_with(JWKS_URL) self.assertEqual(mock_get.call_count, 2) - @patch('requests.get') + @patch("requests.get") def test_fetches_jwks_json_once_on_cache_miss(self, mock_get): - JWKS_URL = 'https://app.myhosting.com/.well-known/jwks.json' + JWKS_URL = "https://app.myhosting.com/.well-known/jwks.json" fetcher = JwksFetcher(JWKS_URL) mock_get.return_value.ok = True @@ -186,15 +217,17 @@ def test_fetches_jwks_json_once_on_cache_miss(self, mock_get): mock_get.return_value.json.return_value = JWKS_RESPONSE_SINGLE_KEY with self.assertRaises(Exception) as err: - key_1 = fetcher.get_key('missing-key') + fetcher.get_key("missing-key") mock_get.assert_called_with(JWKS_URL) - self.assertEqual(str(err.exception), 'RSA Public Key with ID "missing-key" was not found.') + self.assertEqual( + str(err.exception), 'RSA Public Key with ID "missing-key" was not found.' + ) self.assertEqual(mock_get.call_count, 1) - @patch('requests.get') + @patch("requests.get") def test_fails_to_fetch_jwks_json_after_retrying_twice(self, mock_get): - JWKS_URL = 'https://app.myhosting.com/.well-known/jwks.json' + JWKS_URL = "https://app.myhosting.com/.well-known/jwks.json" fetcher = JwksFetcher(JWKS_URL) mock_get.return_value.ok = False @@ -202,32 +235,46 @@ def test_fails_to_fetch_jwks_json_after_retrying_twice(self, mock_get): mock_get.return_value.text = "Some error happened" with self.assertRaises(Exception) as err: - key_1 = fetcher.get_key('id1') + fetcher.get_key("id1") mock_get.assert_called_with(JWKS_URL) - self.assertEqual(str(err.exception), 'RSA Public Key with ID "id1" was not found.') + self.assertEqual( + str(err.exception), 'RSA Public Key with ID "id1" was not found.' + ) self.assertEqual(mock_get.call_count, 2) class TestTokenVerifier(unittest.TestCase): - @staticmethod def asymmetric_signature_verifier_mock(): - verifier = AsymmetricSignatureVerifier('some URL') - verifier._fetch_key = MagicMock('_fetch_key') + verifier = AsymmetricSignatureVerifier("some URL") + verifier._fetch_key = MagicMock("_fetch_key") # noinspection PyUnresolvedReferences # requirement already includes cryptography -> pyjwt[crypto] - verifier._fetch_key.return_value = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(RSA_PUB_KEY_1_JWK)) + verifier._fetch_key.return_value = jwt.algorithms.RSAAlgorithm.from_jwk( + json.dumps(RSA_PUB_KEY_1_JWK) + ) return verifier - def assert_fails_with_error(self, token, error_message, signature_verifier=None, audience=expectations['audience'], issuer=expectations['issuer'], nonce=None, max_age=None, clock=MOCKED_CLOCK, organization=None): + def assert_fails_with_error( + self, + token, + error_message, + signature_verifier=None, + audience=expectations["audience"], + issuer=expectations["issuer"], + nonce=None, + max_age=None, + clock=MOCKED_CLOCK, + organization=None, + ): sv = signature_verifier or self.asymmetric_signature_verifier_mock() tv = TokenVerifier( signature_verifier=sv, issuer=issuer, audience=audience, - leeway=DEFAULT_LEEWAY + leeway=DEFAULT_LEEWAY, ) tv._clock = clock with self.assertRaises(TokenValidationError) as err: @@ -238,8 +285,13 @@ def test_fails_at_creation_with_invalid_signature_verifier(self): sv = "string is not an instance of signature verifier" with self.assertRaises(TypeError) as err: # noinspection PyTypeChecker - TokenVerifier(signature_verifier=sv, issuer="valid issuer", audience="valid audience") - self.assertEqual(str(err.exception), "signature_verifier must be an instance of SignatureVerifier.") + TokenVerifier( + signature_verifier=sv, issuer="valid issuer", audience="valid audience" + ) + self.assertEqual( + str(err.exception), + "signature_verifier must be an instance of SignatureVerifier.", + ) def test_err_token_empty(self): token = "" @@ -261,8 +313,8 @@ def test_HS256_token_signature_passes(self): sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK tv.verify(token) @@ -273,8 +325,8 @@ def test_RS256_token_signature_passes(self): sv = self.asymmetric_signature_verifier_mock() tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK tv.verify(token) @@ -283,51 +335,85 @@ def test_HS256_token_signature_fails(self): token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature" sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) - self.assert_fails_with_error(token, "Invalid token signature.", signature_verifier=sv) + self.assert_fails_with_error( + token, "Invalid token signature.", signature_verifier=sv + ) def test_RS256_token_signature_fails(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature" sv = self.asymmetric_signature_verifier_mock() - self.assert_fails_with_error(token, "Invalid token signature.", signature_verifier=sv) + self.assert_fails_with_error( + token, "Invalid token signature.", signature_verifier=sv + ) def test_fails_with_algorithm_not_supported(self): token = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0." - self.assert_fails_with_error(token, 'Signature algorithm of "none" is not supported. Expected the token to be signed with "RS256"') + self.assert_fails_with_error( + token, + 'Signature algorithm of "none" is not supported. Expected the token to be' + ' signed with "RS256"', + ) return def test_fails_with_iss_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.XuWmo_XxNET0mOW1AaLwi8koUOd05TZULWCGF_3WbeR5VJB6aK0rzo8AkHXrSv9Yr6he_1N8xFDKBIIyXFa4Y2PN8kdwUQtsJcj-cj8_2Ta2S0vV6O7XqbW58eXhX8Ng0OUrqgkHT1leIUJnBZ10YhM0-0zmdIq_WlNnwTdmvAGtYAUGcjyUmq-QEKBc2YYnf83vtGuFT2xGUGsTKR_Jj7lH_QTYdFaiT4t7gwFyXhP5KVUkG3ebdQUkIAQnoY0TXwrgDDCQhAWiUYZehMlv7Ml4tqLsiIUMgm4w5wSfdTdhVEMa7wHPj7gp4s-bfEqaOuyg0xH24rP19LkJROITDw" - self.assert_fails_with_error(token, 'Issuer (iss) claim must be a string present in the ID token') + self.assert_fails_with_error( + token, "Issuer (iss) claim must be a string present in the ID token" + ) def test_fails_with_iss_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzb21ldGhpbmctZWxzZSIsInN1YiI6ImF1dGgwfDEyMzQ1Njc4OSIsImF1ZCI6WyJ0b2tlbnMtdGVzdC0xMjMiLCJleHRlcm5hbC10ZXN0LTk5OSJdLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.HNQ1lXWWQOC4D1-D4XfYlF2MBE6F7TW1EoBqt3ayxehNqtc8ZUJ6MR4aE_o7NnY0aBNbp7J9okgRC2PIKPHZkXUdxHvGZNldrEDDKeKPe0kZFxF5sEK8RYCnJk5m28JFpgRYvXA9KjKnLsBsbV--8VnkgRlw0-LClxqp3ynoGgmh2dVvBqXV8DiAbRvvRPZOg7CVFqCxJoMFD0FJ_dej7ChxMDSe_NDW-CjG9rgEsw_el-_vUcKSp7bzZ1jKm0zOcPDRPfgda5oek0xR6_bg2es_TarYKCwlQCVG1NEmgcJ5gNeVIsrwaPrMXqGr9KNs-nLerQO9Jl1EhCU8No5Sfg" - self.assert_fails_with_error(token, 'Issuer (iss) claim mismatch in the ID token; expected "{}", found "something-else"'.format(expectations['issuer'])) + self.assert_fails_with_error( + token, + 'Issuer (iss) claim mismatch in the ID token; expected "{}", found' + ' "something-else"'.format(expectations["issuer"]), + ) def test_fails_with_sub_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.VtPtxkKh7vZKECzFiNXLWBeWcRgaX1lIOhW0fKU5WSgTcjYZRYoxW-wwI8pai7vgJMQQCizBpLpjMPpCBYBGEiYgGpa9D7vAD8XmYjcVZujG1FLFGkOTkgisCtgVT6WpvwxIejNrl_TcgSEBkCcD9f7gGnFVOjJe4YtSMEUdtuDz-pHEGGNbJLdq0L-pPUrO2Fyw3NspX1RrEYVn7uGuAlDQWQ4x6IOtM40NPzAmyLVrsOPmz_5Igyi7ZZar6epcfd5dBeUbgp8yK178XV-r6-UMuj39NJE4Bx8cDQR1qjxMsxgZ3Lem6OLfFvKWXsgJs7dh13kJDqrx2jXfhvpd-w" - self.assert_fails_with_error(token, 'Subject (sub) claim must be a string present in the ID token') + self.assert_fails_with_error( + token, "Subject (sub) claim must be a string present in the ID token" + ) def test_fails_with_aud_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.t5SBd_J-k_GwPHfyGfPxOcT8n0Pbwy9R-pj7tK8231m3My1Zg3LyKx3tl7MFtymgRHcs2hd4WrWrKjyFrHMOzUWX8dQ2-b6KVRuFQjc70gnW54igj-cT-oo07Lzen0Ol6_7w_5rabWCOL9lM0UM9jpau_llVh97zyYgcUEBeA5lLld5ZLTB-JKMVehjJelBR-MPEDvMr2zT9nRPPUXqezAWZOPYG83oRRB2ktoafaUM4RVvp34q6uUWJq49m-qY2DfKuyDGK4axo1fHKE3JmrsayEDpuGDYDFNDQzy4g1lJvzBKxV2SJl0LKP6sxbM8sw7qaH4ViRNZpFQBZ7veGPQ" - self.assert_fails_with_error(token, 'Audience (aud) claim must be a string or array of strings present in the ID token') + self.assert_fails_with_error( + token, + "Audience (aud) claim must be a string or array of strings present in the" + " ID token", + ) def test_fails_with_aud_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.d1BFaw_h5VOAw0tXAQ98hrru4gWCNjIxcCQktFVcLIqrX9m74-vWv2SVoFBAQlXihEXoDS-5QSMhVPG1iry9arseN16PnSOmilBhSebiqAVSBojLxq5KFEDuUz90lApt4d5BSCMAIAQ1Dp1pGKwJC0BiLrFNOQ2KrmoEvQMgaD0PLlCLy1lL7MntABE86tX_BoqI4ZkWJ1lX1n2-SZAn-ldoOK8W8RUYiwBUDTktpgAfICFUSPAZXj_vn05vwvQBoozhMQkuJrPziz81Tj8lPh0iPsnMBtsAqvAhdwtp3p-eadXPVcjNu4yE3dkBgFDQwoNtV_elWQtmFBn49FEKyw" - self.assert_fails_with_error(token, 'Audience (aud) claim mismatch in the ID token; expected "{}" but was not one of "external-test-999"'.format(expectations['audience'])) + self.assert_fails_with_error( + token, + 'Audience (aud) claim mismatch in the ID token; expected "{}" but was not' + ' one of "external-test-999"'.format(expectations["audience"]), + ) def test_fails_with_exp_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.qq4Tyz8VGnAJy2KmKo3zmThnWXviGwJB-Bz18lnJlicjAXk4ObeUVoX0WHB3I8vmDMscC9JXhkDP5KIGSAkeaLzil0FFJwwQB_saFE_y3zjfMQgdsz_qtDR96S1SQtnZ7AhChIAZ7cV4p_wmj_-TQYuZVQ0F6MoBmFsJITIr7gxnNFJWw6KJEU94nq8-q_IAyC5-9epdEtphUi_wkalHzEy6w8tQawKXCLYxo12VNlEy5mi8PlwqGsqVcwFwqec-ERt2hajyuqL1210-cZJMA-NmXz4mv4scHdiE09KZcLScKcezs9KaM5iaButMBL0Fyec0KcwwfELX_zghekALOA" - self.assert_fails_with_error(token, 'Expiration Time (exp) claim must be a number present in the ID token') + self.assert_fails_with_error( + token, + "Expiration Time (exp) claim must be a number present in the ID token", + ) def test_fails_with_exp_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NTkyNTYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.KUbd2s3Km-PVpP8KEJo1e0lyQv19TjiKMFX-lVebFoiPNwlVTXS08g5qe_G8pcOrwNfX6cRkRLbp7TNQ7tGDCuEcdia9KOaWeVWla5B3UPCv1qozCyMv4ZYrA0qdT2KgwytRMVWSov9ly29FSo6SRQksAMKZdnAzPaqnJGKBgVIjKN3a5ePIeX5yBIGxlNjS3nyWt8LIQJ9BFaQWk3i0vAKYpDeco3VLNLX-wH7739MzS7ll6t6LyuZi6kBaRG6XZc394glKidTvCp06ViQlPlcuV7JsCJfbkBc0AS5TmzOEdUCype-gzNqbuLcSXihS-qOx7Yjv8y3farV1_7qYqw" mocked_clock = MOCKED_CLOCK + DEFAULT_LEEWAY + 1 - self.assert_fails_with_error(token, 'Expiration Time (exp) claim error in the ID token; current time ({}) is after expiration time (1587592621)'.format(mocked_clock), clock=mocked_clock) + self.assert_fails_with_error( + token, + "Expiration Time (exp) claim error in the ID token; current time ({}) is" + " after expiration time (1587592621)".format(mocked_clock), + clock=mocked_clock, + ) def test_fails_with_iat_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.CWW7mWUhiI-rramQ2dIGi7vBsOMmsouIg32IL9u2g4Dg3PV0C55R7dL6Jvf9hqaZXvx9Psrw0vLnOlhFztAC6LlQuq2eCaLYsDme36NxeYGC7CFXygvlU_eXD5IdNK35GriTfpG_b5hC7tl2glMmNQcZWlsIyKw7eq8o1POpqo0K2bCVjoyJkHL6WUpw6_08HPspmTL_Qd0km08z6zgvbl8Hpzk-tLmXqN7LjmuhEsjnIFphu-dGwcQsoY3RAomYxAFXAPYT8siEIf2w3zlIoUde-mujiMUtMD-Od7t7w36GO6Kubb9M9inVwPEg1yFKlFTXZBKXu91xmOmvMJ5Qfg" - self.assert_fails_with_error(token, 'Issued At (iat) claim must be a number present in the ID token') + self.assert_fails_with_error( + token, "Issued At (iat) claim must be a number present in the ID token" + ) def test_passes_when_nonce_missing_but_not_required(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.L-DveLCDf4Te7x3JZmQ6rCkUQrenl1NFpHqKD8Fs-glhd2iyc-TYffk1M30T0-wBri-3tTgraDAjZAjXuwSk0gV_V5uKCHyIoSRphrC88aX8IeECteQpHa4KR15lbzA5JdVhJu7LuCZ2EFvdjHh5GiViLRWsTSHGUM-uqcMK0q2kWGvCEgfOIXqocnQiyCNITxfgMYJd38zOsVeP7HFf9riuFEQz65oER22o3xyIZ-ILSaU10n6Ob559Rbjc0NVKH4hrggRg8kG7cJCiXbRxXnzO_VM8LmRHhF56jh3ZSrO4bzQa5xv04bMbX6A77muMZD0vghsaslvpWerWbwaSQQ" @@ -335,32 +421,53 @@ def test_passes_when_nonce_missing_but_not_required(self): sv = self.asymmetric_signature_verifier_mock() tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK tv.verify(token) def test_fails_with_nonce_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.L-DveLCDf4Te7x3JZmQ6rCkUQrenl1NFpHqKD8Fs-glhd2iyc-TYffk1M30T0-wBri-3tTgraDAjZAjXuwSk0gV_V5uKCHyIoSRphrC88aX8IeECteQpHa4KR15lbzA5JdVhJu7LuCZ2EFvdjHh5GiViLRWsTSHGUM-uqcMK0q2kWGvCEgfOIXqocnQiyCNITxfgMYJd38zOsVeP7HFf9riuFEQz65oER22o3xyIZ-ILSaU10n6Ob559Rbjc0NVKH4hrggRg8kG7cJCiXbRxXnzO_VM8LmRHhF56jh3ZSrO4bzQa5xv04bMbX6A77muMZD0vghsaslvpWerWbwaSQQ" - self.assert_fails_with_error(token, 'Nonce (nonce) claim must be a string present in the ID token', nonce=expectations['nonce']) + self.assert_fails_with_error( + token, + "Nonce (nonce) claim must be a string present in the ID token", + nonce=expectations["nonce"], + ) def test_fails_with_nonce_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiMDAwOTk5IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.Z0n4vKcTCTrKJXUx56TyoQk3-okpjOqCDRN9NqH5zkCi0at7WV-E3TxGbXpIN0UsXwUWxrHiuvL9lN2M4PoIvL4XvzUqgsepAYYBCPGR9Wxb-ALmhhWdS_LNRVAgfUCn94khc_G51XtyeP0bQgWRkV7VbeWxkBTnrhmGwEkVx6XbfpnTRUCDSR_luJfUu84LkFJf1n2ohnEU7Q74BXJjxIIJnhZrg4J65E3cNtZ9N7AOIrbpbZ0oB7NhcZP0xA0A75qt7ZnKOuLsbRppZjcz56QmVIArOSCkkegl3qLx4cNdVa-O840AQWCwkAcHxS9lHBIWyaToC7IVMOLxIcGVlQ" - self.assert_fails_with_error(token, 'Nonce (nonce) claim mismatch in the ID token; expected "{}", found "000999"'.format(expectations['nonce']), nonce=expectations['nonce']) + self.assert_fails_with_error( + token, + 'Nonce (nonce) claim mismatch in the ID token; expected "{}", found' + ' "000999"'.format(expectations["nonce"]), + nonce=expectations["nonce"], + ) def test_fails_with_aud_array_and_azp_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Xpxc2tj3sDwAYftYAcoiLO3kq0X54KSngDzQu_foTjlDQFTPApVVrX_BQqMAUFsmiNdt-3Tf9lkUlAagpvXy_VUY5LIjzEihEKDzqQFMQ8wm7RK7qV2XLS1abltxXd8AuOHcPnVHbtERpsCXR5eRt0-ESSPUw_scqHOwmYQOFF0sOQJ72r9EYZFMGhojyzpbzhBF0jgi9wMqj0VSpLEzZ3MnkWBCTlmc5OAPXQGdq6C1dVfcBXy4iiIBEaPCG962Yqrr-bbL92_T_XG-FmtpIViuRjHDWRJ26uoD0cmXwePwojxlyeY7VrAoKX3VtA4lm1Co0BMh9DjMKv-p3zT6XA" - self.assert_fails_with_error(token, 'Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values') + self.assert_fails_with_error( + token, + "Authorized Party (azp) claim must be a string present in the ID token when" + " Audience (aud) claim has multiple values", + ) def test_fails_with_aud_array_and_azp_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6ImV4dGVybmFsLXRlc3QtOTk5IiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.Bx56kdY8rBwlAZ8Walh6NjONs94Tdv37iP0EPKFxvpELFt_8RENhPp8Lqe52zrrgXqUdA1eeBynegqH7_duJawQ0l86u2dsaPonMOsh_W8ZjaqPOVHLv1z7xQb84UdjMSSJbMGMLPmuX2GMlc5hcjW5YgWU1xp-gpNpKMIzW19gNxpwtIWkLZ5zkjEVBYHSTAw7CO6HkncTZBqdYA3bq3ziQPljqvSvyPehuJ-2Q5TlrdVLRO5HS4-C6NEs-h8fpX25NP537FM9g7T7pRB1wDxsrJTny6uKBKFCwtNSF5laojV2edEDlDUsEEUCGh6zUzITGeZNa0M52ZsxGoAehIw" - self.assert_fails_with_error(token, - 'Authorized Party (azp) claim mismatch in the ID token; expected "{}", found "external-test-999"'.format(expectations['audience'])) + self.assert_fails_with_error( + token, + 'Authorized Party (azp) claim mismatch in the ID token; expected "{}",' + ' found "external-test-999"'.format(expectations["audience"]), + ) def test_fails_when_max_age_sent_with_auth_time_missing(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyJ9.pWr6hjQ9Mi9cYSYIFWG1d-KJ98OeKeKb3F7X_DdUgQ5Xir8wHLLuZDrFAalKlxYiTJTMXqh9YkxKLcEjFQsTECEXA4VMliUv4A2Egk8EDUi5SQtoQ11xGJo-S7qM4cL-x-69ZnJvNWlZZ8NnvtTOSgzpa_fsG7T3PScr0b9ukxOQ-o8suV2fLE7bOliBKZn9PC7sowtF_oeQ03f0e0thtZFl121ROL65ARh9C-ic2azgmIn3YgsvguaoT5ZpAFRe2McaP026bNimOmfEzVIwHCDuR6rkFZvX4QUduX6UQ4bOQp_EC9G0XDk08H0nBXx2JXSHCW4YPZz9bz1f12B4kg" - self.assert_fails_with_error(token, "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified", max_age=120) + self.assert_fails_with_error( + token, + "Authentication Time (auth_time) claim must be a number present in the ID" + " token when Max Age (max_age) is specified", + max_age=120, + ) def test_fails_when_max_age_sent_with_auth_time_invalid(self): token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzU5MjU2MX0.VcVv1cGthPtuHZAZa-x7XZjhrcKEV3xUW6rVfhNMM_zCRBxLdyJl6gVv396eyfMX5-3dhr0-9kAYQAjPrmcvUDFLOR4Qjamg83U-TMa4agnYwQ_Iv3u_zhYmSrKzZlQvbOhT5imZShL11hycyukv2D1ODbFpvCdsHWXUFF0LotiXrBRr45AoEie2bASNSMmCZbQh-_Pq7gdhKOZMhBTErrk-aEOZrmsUG0sL2ZcVLdZ0_U-23ysR2GVpNg8jyv1HLZaPw5IJC4XucRw5r-5UiIcIIdxbUNphamPgq1cqL3QP_UsCGotCIUQTDNMbXB7-J_opBM2uGFp-cW95-Wq7qg" @@ -368,15 +475,22 @@ def test_fails_when_max_age_sent_with_auth_time_invalid(self): expected_auth_time = MOCKED_CLOCK + DEFAULT_LEEWAY + max_age mocked_clock = expected_auth_time + 1 - self.assert_fails_with_error(token, "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time ({}) is after last auth at ({})".format(mocked_clock, expected_auth_time), max_age=max_age, clock=mocked_clock) + self.assert_fails_with_error( + token, + "Authentication Time (auth_time) claim in the ID token indicates that too" + " much time has passed since the last end-user authentication. Current time" + " ({}) is after last auth at ({})".format(mocked_clock, expected_auth_time), + max_age=max_age, + clock=mocked_clock, + ) def test_passes_when_org_present_but_not_required(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ" sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK tv.verify(token) @@ -386,38 +500,95 @@ def test_passes_when_org_present_and_matches(self): sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], + ) + tv._clock = MOCKED_CLOCK + tv.verify(token, organization="org_123") + + def test_fails_when_org_name_specified_but_not_present(self): + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.wotJnUdD5IfdZMewF_-BnHc0pI56uwzwr5qaSXvSu9w" + self.assert_fails_with_error( + token, + "Organization (org_name) claim must be a string present in the ID token", + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org-123", + ) + + def test_fails_when_org_name_specified_but_not_string(self): + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6NDIsImlzcyI6Imh0dHBzOi8vdG9rZW5zLXRlc3QuYXV0aDAuY29tLyIsImV4cCI6MTU4Nzc2NTM2MSwiaWF0IjoxNTg3NTkyNTYxfQ.RXu-dz1u2pftk_iInk1To8z9g1B6TVA-5FAwoCx85T0" + self.assert_fails_with_error( + token, + "Organization (org_name) claim must be a string present in the ID token", + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org-123", + ) + + def test_fails_when_org_name_specified_but_does_not_match(self): + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6Im9yZy1hYmMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.P_ldJGEaFg58cARwGMtog_KTsqv7cGJZXoS9xdTEkvQ" + self.assert_fails_with_error( + token, + 'Organization (org_name) claim mismatch in the ID token; expected "org-123",' + ' found "org-abc"', + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org-123", + ) + + def test_succeeds_when_org_name_specified_matches(self): + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6Im9yZy0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.P8Kba8Fgamyiw1qw_lBfp2OAzWn6NOLL6fBCDQhGvyc" + sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) + tv = TokenVerifier( + signature_verifier=sv, + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK - tv.verify(token, organization='org_123') + response = tv.verify(token) + self.assertIn("org_name", response) + self.assertEqual("org-123", response["org_name"]) - def test_fails_when_org_specified_but_not_present(self): + def test_fails_when_org_id_specified_but_not_present(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.wotJnUdD5IfdZMewF_-BnHc0pI56uwzwr5qaSXvSu9w" - self.assert_fails_with_error(token, "Organization (org_id) claim must be a string present in the ID token", signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_123') + self.assert_fails_with_error( + token, + "Organization (org_id) claim must be a string present in the ID token", + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org_123", + ) - def test_fails_when_org_specified_but_not_(self): + def test_fails_when_org_id_specified_but_not_string(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOjQyLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.fGL1_akaHikdovS7NRYla3flne1xdtCjP0ei_CRxO6k" - self.assert_fails_with_error(token, "Organization (org_id) claim must be a string present in the ID token", signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_123') + self.assert_fails_with_error( + token, + "Organization (org_id) claim must be a string present in the ID token", + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org_123", + ) - def test_fails_when_org_specified_but_does_not_match(self): + def test_fails_when_org_id_specified_but_does_not_match(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ" - self.assert_fails_with_error(token, 'Organization (org_id) claim mismatch in the ID token; expected "org_abc", found "org_123"', signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_abc') + self.assert_fails_with_error( + token, + 'Organization (org_id) claim mismatch in the ID token; expected "org_abc",' + ' found "org_123"', + signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), + organization="org_abc", + ) def test_verify_returns_payload(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ" sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET) tv = TokenVerifier( signature_verifier=sv, - issuer=expectations['issuer'], - audience=expectations['audience'] + issuer=expectations["issuer"], + audience=expectations["audience"], ) tv._clock = MOCKED_CLOCK response = tv.verify(token) - self.assertIn('sub', response); - self.assertIn('aud', response); - self.assertIn('org_id', response); - self.assertIn('iss', response); - self.assertIn('exp', response); - self.assertIn('iat', response); - self.assertEqual('org_123', response['org_id']) + self.assertIn("sub", response) + self.assertIn("aud", response) + self.assertIn("org_id", response) + self.assertIn("iss", response) + self.assertIn("exp", response) + self.assertIn("iat", response) + self.assertEqual("org_123", response["org_id"]) diff --git a/auth0/test/authentication/test_users.py b/auth0/test/authentication/test_users.py new file mode 100644 index 00000000..043595da --- /dev/null +++ b/auth0/test/authentication/test_users.py @@ -0,0 +1,17 @@ +import unittest +from unittest import mock + +from ...authentication.users import Users + + +class TestUsers(unittest.TestCase): + @mock.patch("auth0.rest.RestClient.get") + def test_userinfo(self, mock_get): + u = Users("my.domain.com") + + u.userinfo(access_token="atk") + + mock_get.assert_called_with( + url="https://my.domain.com/userinfo", + headers={"Authorization": "Bearer atk"}, + ) diff --git a/auth0/test/conftest.py b/auth0/test/conftest.py new file mode 100644 index 00000000..1247142f --- /dev/null +++ b/auth0/test/conftest.py @@ -0,0 +1,7 @@ +import pytest +import random + +@pytest.fixture(autouse=True) +def set_random_seed(): + random.seed(42) + print("Random seeded to 42") \ No newline at end of file diff --git a/auth0/v3/test/management/__init__.py b/auth0/test/management/__init__.py similarity index 100% rename from auth0/v3/test/management/__init__.py rename to auth0/test/management/__init__.py diff --git a/auth0/test/management/test_actions.py b/auth0/test/management/test_actions.py new file mode 100644 index 00000000..c66babc4 --- /dev/null +++ b/auth0/test/management/test_actions.py @@ -0,0 +1,249 @@ +import unittest +from unittest import mock + +from ...management.actions import Actions + + +class TestActions(unittest.TestCase): + def test_init_with_optionals(self): + t = Actions(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_actions(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + + c.get_actions() + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/actions/actions", args[0]) + self.assertEqual( + kwargs["params"], + { + "triggerId": None, + "actionName": None, + "deployed": None, + "installed": "false", + "page": None, + "per_page": None, + }, + ) + + c.get_actions("trigger-id", "action-name", True, True, 0, 5) + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/actions/actions", args[0]) + self.assertEqual( + kwargs["params"], + { + "triggerId": "trigger-id", + "actionName": "action-name", + "deployed": "true", + "installed": "true", + "page": 0, + "per_page": 5, + }, + ) + + c.get_actions("trigger-id", "action-name", False, True, 0, 5) + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/actions/actions", args[0]) + self.assertEqual( + kwargs["params"], + { + "triggerId": "trigger-id", + "actionName": "action-name", + "deployed": "false", + "installed": "true", + "page": 0, + "per_page": 5, + }, + ) + + @mock.patch("auth0.management.actions.RestClient") + def test_create_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.create_action({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/actions/actions", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.actions.RestClient") + def test_update_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.update_action("action-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/actions/actions/action-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_action("action-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/actions/actions/action-id", args[0]) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_triggers(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_triggers() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/actions/triggers", args[0]) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.actions.RestClient") + def test_delete_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.delete_action("action-id") + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/actions/actions/action-id", args[0]) + self.assertEqual(kwargs["params"], {"force": "false"}) + + c.delete_action("action-id", True) + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/actions/actions/action-id", args[0]) + self.assertEqual(kwargs["params"], {"force": "true"}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_execution(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_execution("execution-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/executions/execution-id", args[0] + ) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_action_versions(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_action_versions("action-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/actions/action-id/versions", args[0] + ) + self.assertEqual(kwargs["params"], {"page": None, "per_page": None}) + + c.get_action_versions("action-id", 0, 5) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/actions/action-id/versions", args[0] + ) + self.assertEqual(kwargs["params"], {"page": 0, "per_page": 5}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_trigger_bindings(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_trigger_bindings("trigger-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/triggers/trigger-id/bindings", args[0] + ) + self.assertEqual(kwargs["params"], {"page": None, "per_page": None}) + + c.get_trigger_bindings("trigger-id", 0, 5) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/triggers/trigger-id/bindings", args[0] + ) + self.assertEqual(kwargs["params"], {"page": 0, "per_page": 5}) + + @mock.patch("auth0.management.actions.RestClient") + def test_get_action_version(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.get_action_version("action-id", "version-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/actions/actions/action-id/versions/version-id", + args[0], + ) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.actions.RestClient") + def test_deploy_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.deploy_action("action-id") + + args, kwargs = mock_instance.post.call_args + + self.assertEqual( + "https://domain/api/v2/actions/actions/action-id/deploy", args[0] + ) + + @mock.patch("auth0.management.actions.RestClient") + def test_rollback_action(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.rollback_action_version("action-id", "version-id") + + args, kwargs = mock_instance.post.call_args + + self.assertEqual( + "https://domain/api/v2/actions/actions/action-id/versions/version-id/deploy", + args[0], + ) + self.assertEqual(kwargs["data"], {}) + + @mock.patch("auth0.management.actions.RestClient") + def test_update_trigger_bindings(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Actions(domain="domain", token="jwttoken") + c.update_trigger_bindings("trigger-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual( + "https://domain/api/v2/actions/triggers/trigger-id/bindings", args[0] + ) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) diff --git a/auth0/test/management/test_atack_protection.py b/auth0/test/management/test_atack_protection.py new file mode 100644 index 00000000..58a50d55 --- /dev/null +++ b/auth0/test/management/test_atack_protection.py @@ -0,0 +1,96 @@ +import unittest +from unittest import mock + +from ...management.attack_protection import AttackProtection + + +class TestAttackProtection(unittest.TestCase): + def test_init_with_optionals(self): + t = AttackProtection( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_get_breached_password_detection(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + ap = AttackProtection(domain="domain", token="jwttoken") + ap.get_breached_password_detection() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/attack-protection/breached-password-detection", + args[0], + ) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_update_breached_password_detection(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.patch.return_value = {} + + c = AttackProtection(domain="domain", token="jwttoken") + c.update_breached_password_detection({"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/attack-protection/breached-password-detection", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_get_brute_force_protection(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + ap = AttackProtection(domain="domain", token="jwttoken") + ap.get_brute_force_protection() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/attack-protection/brute-force-protection", args[0] + ) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_update_brute_force_protection(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.patch.return_value = {} + + c = AttackProtection(domain="domain", token="jwttoken") + c.update_brute_force_protection({"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/attack-protection/brute-force-protection", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_get_suspicious_ip_throttling(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + ap = AttackProtection(domain="domain", token="jwttoken") + ap.get_suspicious_ip_throttling() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/attack-protection/suspicious-ip-throttling", args[0] + ) + + @mock.patch("auth0.management.attack_protection.RestClient") + def test_update_suspicious_ip_throttling(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.patch.return_value = {} + + c = AttackProtection(domain="domain", token="jwttoken") + c.update_suspicious_ip_throttling({"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/attack-protection/suspicious-ip-throttling", + data={"a": "b", "c": "d"}, + ) diff --git a/auth0/v3/test/management/test_auth0.py b/auth0/test/management/test_auth0.py similarity index 87% rename from auth0/v3/test/management/test_auth0.py rename to auth0/test/management/test_auth0.py index 0822374e..4a4c9859 100644 --- a/auth0/v3/test/management/test_auth0.py +++ b/auth0/test/management/test_auth0.py @@ -1,9 +1,10 @@ import unittest -from ...management.auth0 import Auth0 from ...management.actions import Actions from ...management.attack_protection import AttackProtection +from ...management.auth0 import Auth0 from ...management.blacklists import Blacklists +from ...management.client_credentials import ClientCredentials from ...management.client_grants import ClientGrants from ...management.clients import Clients from ...management.connections import Connections @@ -17,6 +18,7 @@ from ...management.jobs import Jobs from ...management.log_streams import LogStreams from ...management.logs import Logs +from ...management.network_acls import NetworkAcls from ...management.organizations import Organizations from ...management.prompts import Prompts from ...management.resource_servers import ResourceServers @@ -29,6 +31,7 @@ from ...management.user_blocks import UserBlocks from ...management.users import Users from ...management.users_by_email import UsersByEmail +from ...rest import RestClientOptions class TestAuth0(unittest.TestCase): @@ -46,6 +49,9 @@ def test_attack_protection(self): def test_blacklists(self): self.assertIsInstance(self.a0.blacklists, Blacklists) + def test_client_credentials(self): + self.assertIsInstance(self.a0.client_credentials, ClientCredentials) + def test_client_grants(self): self.assertIsInstance(self.a0.client_grants, ClientGrants) @@ -84,6 +90,9 @@ def test_log_streams(self): def test_logs(self): self.assertIsInstance(self.a0.logs, Logs) + + def test_network_acls(self): + self.assertIsInstance(self.a0.network_acls, NetworkAcls) def test_organizations(self): self.assertIsInstance(self.a0.organizations, Organizations) @@ -120,3 +129,8 @@ def test_users_by_email(self): def test_users(self): self.assertIsInstance(self.a0.users, Users) + + def test_args(self): + rest_options = RestClientOptions(retries=99) + auth0 = Auth0(self.domain, self.token, rest_options=rest_options) + self.assertEqual(auth0.users.client.options.retries, 99) diff --git a/auth0/test/management/test_blacklists.py b/auth0/test/management/test_blacklists.py new file mode 100644 index 00000000..aff79203 --- /dev/null +++ b/auth0/test/management/test_blacklists.py @@ -0,0 +1,47 @@ +import unittest +from unittest import mock + +from ...management.blacklists import Blacklists + + +class TestBlacklists(unittest.TestCase): + def test_init_with_optionals(self): + t = Blacklists( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.blacklists.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + t = Blacklists(domain="domain", token="jwttoken") + t.get(aud="an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/blacklists/tokens", params={"aud": "an-id"} + ) + + @mock.patch("auth0.management.blacklists.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + t = Blacklists(domain="domain", token="jwttoken") + + # create without audience + t.create(jti="the-jti") + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/blacklists/tokens", args[0]) + self.assertEqual(kwargs["data"], {"jti": "the-jti"}) + + # create with audience + t.create(jti="the-jti", aud="the-aud") + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/blacklists/tokens", args[0]) + self.assertEqual(kwargs["data"], {"jti": "the-jti", "aud": "the-aud"}) diff --git a/auth0/test/management/test_branding.py b/auth0/test/management/test_branding.py new file mode 100644 index 00000000..daf2ac75 --- /dev/null +++ b/auth0/test/management/test_branding.py @@ -0,0 +1,138 @@ +import unittest +from unittest import mock + +from ...management.branding import Branding + + +class TestBranding(unittest.TestCase): + def test_init_with_optionals(self): + branding = Branding( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(branding.client.options.timeout, (10, 2)) + + telemetry = branding.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry, None) + + @mock.patch("auth0.management.branding.RestClient") + def test_get(self, mock_rc): + api = mock_rc.return_value + + branding = Branding(domain="domain", token="jwttoken") + branding.get() + + api.get.assert_called_with( + "https://domain/api/v2/branding", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_update(self, mock_rc): + api = mock_rc.return_value + api.patch.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.update({"a": "b", "c": "d"}) + + api.patch.assert_called_with( + "https://domain/api/v2/branding", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_get_template_universal_login(self, mock_rc): + api = mock_rc.return_value + + branding = Branding(domain="domain", token="jwttoken") + branding.get_template_universal_login() + + api.get.assert_called_with( + "https://domain/api/v2/branding/templates/universal-login", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_delete_template_universal_login(self, mock_rc): + api = mock_rc.return_value + + branding = Branding(domain="domain", token="jwttoken") + branding.delete_template_universal_login() + + api.delete.assert_called_with( + "https://domain/api/v2/branding/templates/universal-login", + ) + + @mock.patch("auth0.rest.requests.request") + def test_update_template_universal_login(self, mock_rc): + mock_rc.return_value.status_code = 200 + mock_rc.return_value.text = "{}" + + branding = Branding(domain="domain", token="jwttoken") + branding.update_template_universal_login({"a": "b", "c": "d"}) + + mock_rc.assert_called_with( + "PUT", + "https://domain/api/v2/branding/templates/universal-login", + json={"template": {"a": "b", "c": "d"}}, + headers=mock.ANY, + timeout=5.0, + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_get_default_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.get.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.get_default_branding_theme() + + api.get.assert_called_with( + "https://domain/api/v2/branding/themes/default", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_get_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.get.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.get_branding_theme("theme_id") + + api.get.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_delete_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.delete.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.delete_branding_theme("theme_id") + + api.delete.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_update_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.patch.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.update_branding_theme("theme_id", {}) + + api.patch.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + data={}, + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_create_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.post.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.create_branding_theme({}) + + api.post.assert_called_with( + "https://domain/api/v2/branding/themes", + data={}, + ) diff --git a/auth0/test/management/test_client_credentials.py b/auth0/test/management/test_client_credentials.py new file mode 100644 index 00000000..97db0e1b --- /dev/null +++ b/auth0/test/management/test_client_credentials.py @@ -0,0 +1,50 @@ +import unittest +from unittest import mock + +from ...management.client_credentials import ClientCredentials + + +class TestClientCredentials(unittest.TestCase): + def test_init_with_optionals(self): + t = ClientCredentials( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.client_credentials.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + c = ClientCredentials(domain="domain", token="jwttoken") + c.all("cid") + mock_instance.get.assert_called_with( + "https://domain/api/v2/clients/cid/credentials" + ) + + @mock.patch("auth0.management.client_credentials.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + c = ClientCredentials(domain="domain", token="jwttoken") + c.get("cid", "this-id") + mock_instance.get.assert_called_with( + "https://domain/api/v2/clients/cid/credentials/this-id" + ) + + @mock.patch("auth0.management.client_credentials.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + c = ClientCredentials(domain="domain", token="jwttoken") + c.create("cid", {"a": "b", "c": "d"}) + mock_instance.post.assert_called_with( + "https://domain/api/v2/clients/cid/credentials", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.client_credentials.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + c = ClientCredentials(domain="domain", token="jwttoken") + c.delete("cid", "this-id") + mock_instance.delete.assert_called_with( + "https://domain/api/v2/clients/cid/credentials/this-id" + ) diff --git a/auth0/test/management/test_client_grants.py b/auth0/test/management/test_client_grants.py new file mode 100644 index 00000000..eeef4f7b --- /dev/null +++ b/auth0/test/management/test_client_grants.py @@ -0,0 +1,167 @@ +import unittest +from unittest import mock + +from ...management.client_grants import ClientGrants + + +class TestClientGrants(unittest.TestCase): + def test_init_with_optionals(self): + t = ClientGrants( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + + # With default params + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": None, + "page": None, + "per_page": None, + "include_totals": "false", + "client_id": None, + "allow_any_organization": None, + }, + ) + + # With audience + c.all(audience="http://domain.auth0.com/api/v2/") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": "http://domain.auth0.com/api/v2/", + "page": None, + "per_page": None, + "include_totals": "false", + "client_id": None, + "allow_any_organization": None, + }, + ) + + # With pagination params + c.all(per_page=23, page=7, include_totals=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": None, + "page": 7, + "per_page": 23, + "include_totals": "true", + "client_id": None, + "allow_any_organization": None, + }, + ) + + # With client_id param + c.all(client_id="exampleid") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": None, + "page": None, + "per_page": None, + "include_totals": "false", + "client_id": "exampleid", + "allow_any_organization": None, + }, + ) + + # With allow any organization + c.all(allow_any_organization=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": None, + "page": None, + "per_page": None, + "include_totals": "false", + "client_id": None, + "allow_any_organization": True, + }, + ) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/client-grants", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + c.delete("this-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/client-grants/this-id" + ) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + c.update("this-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/client-grants/this-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_get_organizations(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + c.get_organizations("cgid") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/client-grants/cgid/organizations", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "false", + "from": None, + "take": None, + }, + ) diff --git a/auth0/test/management/test_clients.py b/auth0/test/management/test_clients.py new file mode 100644 index 00000000..41c164a9 --- /dev/null +++ b/auth0/test/management/test_clients.py @@ -0,0 +1,136 @@ +import unittest +from unittest import mock + +from ...management.clients import Clients + + +class TestClients(unittest.TestCase): + def test_init_with_optionals(self): + t = Clients(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.clients.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients", args[0]) + self.assertEqual( + kwargs["params"], + {"fields": None, "include_fields": "true", "page": None, "per_page": None}, + ) + + # Fields filter + c.all(fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": "a,b", + "include_fields": "false", + "page": None, + "per_page": None, + }, + ) + + # Specific pagination + c.all(page=7, per_page=25) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients", args[0]) + self.assertEqual( + kwargs["params"], + {"fields": None, "include_fields": "true", "page": 7, "per_page": 25}, + ) + + # Extra parameters + c.all(extra_params={"some_key": "some_value"}) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "page": None, + "per_page": None, + "some_key": "some_value", + }, + ) + + @mock.patch("auth0.management.clients.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/clients", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.clients.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + c.get("this-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients/this-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + c.get("this-id", fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/clients/this-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + @mock.patch("auth0.management.clients.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + c.delete("this-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/clients/this-id") + + @mock.patch("auth0.management.clients.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + c.update("this-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/clients/this-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.clients.RestClient") + def test_rotate_secret(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Clients(domain="domain", token="jwttoken") + c.rotate_secret("this-id") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/clients/this-id/rotate-secret", + data={"id": "this-id"}, + ) diff --git a/auth0/test/management/test_connections.py b/auth0/test/management/test_connections.py new file mode 100644 index 00000000..1f27de69 --- /dev/null +++ b/auth0/test/management/test_connections.py @@ -0,0 +1,202 @@ +import unittest +from unittest import mock + +from ...management.connections import Connections + + +class TestConnection(unittest.TestCase): + def test_init_with_optionals(self): + t = Connections( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.connections.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + # Default parameters are requested + c = Connections(domain="domain", token="jwttoken") + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "strategy": None, + "page": None, + "per_page": None, + "include_fields": "true", + "name": None, + }, + ) + + # Fields filter + c.all(fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": "a,b", + "strategy": None, + "page": None, + "per_page": None, + "include_fields": "false", + "name": None, + }, + ) + + # Fields + strategy filter + c.all(fields=["a", "b"], strategy="auth0", include_fields=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": "a,b", + "strategy": "auth0", + "page": None, + "per_page": None, + "include_fields": "true", + "name": None, + }, + ) + + # Specific pagination + c.all(page=7, per_page=25) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "strategy": None, + "page": 7, + "per_page": 25, + "include_fields": "true", + "name": None, + }, + ) + + # Extra parameters + c.all(extra_params={"some_key": "some_value"}) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "strategy": None, + "page": None, + "per_page": None, + "include_fields": "true", + "some_key": "some_value", + "name": None, + }, + ) + + # Name + c.all(name="foo") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/connections", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "strategy": None, + "page": None, + "per_page": None, + "include_fields": "true", + "name": "foo", + }, + ) + + @mock.patch("auth0.management.connections.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + c = Connections(domain="domain", token="jwttoken") + c.get("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/connections/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + c.get("an-id", fields=["a", "b"]) + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/connections/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "true"}) + + c.get("an-id", fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/connections/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + @mock.patch("auth0.management.connections.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.delete.return_value = {} + + c = Connections(domain="domain", token="jwttoken") + c.delete("this-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/connections/this-id" + ) + + @mock.patch("auth0.management.connections.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.patch.return_value = {} + + c = Connections(domain="domain", token="jwttoken") + c.update("that-id", {"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/connections/that-id", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.connections.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.post.return_value = {} + + c = Connections(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/connections", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.connections.RestClient") + def test_delete_user_by_email(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.delete_user_by_email.return_value = {} + + c = Connections(domain="domain", token="jwttoken") + c.delete_user_by_email("123", "test@example.com") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/connections/123/users", + params={"email": "test@example.com"}, + ) diff --git a/auth0/test/management/test_custom_domains.py b/auth0/test/management/test_custom_domains.py new file mode 100644 index 00000000..4dc8f975 --- /dev/null +++ b/auth0/test/management/test_custom_domains.py @@ -0,0 +1,57 @@ +import unittest +from unittest import mock + +from ...management.custom_domains import CustomDomains + + +class TestCustomDomains(unittest.TestCase): + def test_init_with_optionals(self): + t = CustomDomains( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.custom_domains.RestClient") + def test_get_all(self, mock_rc): + mock_instance = mock_rc.return_value + + g = CustomDomains(domain="domain", token="jwttoken") + g.all() + + mock_instance.get.assert_called_with("https://domain/api/v2/custom-domains") + + @mock.patch("auth0.management.custom_domains.RestClient") + def test_create_new(self, mock_rc): + mock_instance = mock_rc.return_value + + g = CustomDomains(domain="domain", token="jwttoken") + g.create_new(body={"a": "b", "c": "d", "e": "f"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/custom-domains", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d", "e": "f"}) + + @mock.patch("auth0.management.custom_domains.RestClient") + def test_get_domain_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + g = CustomDomains(domain="domain", token="jwttoken") + g.get("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/custom-domains/an-id" + ) + + @mock.patch("auth0.management.custom_domains.RestClient") + def test_verify(self, mock_rc): + mock_instance = mock_rc.return_value + + g = CustomDomains(domain="domain", token="jwttoken") + g.verify("an-id") + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/custom-domains/an-id/verify", args[0]) diff --git a/auth0/test/management/test_device_credentials.py b/auth0/test/management/test_device_credentials.py new file mode 100644 index 00000000..1b524cd3 --- /dev/null +++ b/auth0/test/management/test_device_credentials.py @@ -0,0 +1,87 @@ +import unittest +from unittest import mock + +from ...management.device_credentials import DeviceCredentials + + +class TestDeviceCredentials(unittest.TestCase): + def test_init_with_optionals(self): + t = DeviceCredentials( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.device_credentials.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = DeviceCredentials(domain="domain", token="jwttoken") + c.get(user_id="uid", client_id="cid", type="type", page=0, per_page=20) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/device-credentials", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "user_id": "uid", + "client_id": "cid", + "type": "type", + "page": 0, + "per_page": 20, + "include_totals": "false", + }, + ) + + c.get( + user_id="uid", + client_id="cid", + type="type", + page=5, + per_page=50, + include_totals=True, + ) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/device-credentials", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "user_id": "uid", + "client_id": "cid", + "type": "type", + "page": 5, + "per_page": 50, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.device_credentials.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = DeviceCredentials(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/device-credentials", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.device_credentials.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = DeviceCredentials(domain="domain", token="jwttoken") + c.delete("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/device-credentials/an-id", + ) diff --git a/auth0/test/management/test_email_endpoints.py b/auth0/test/management/test_email_endpoints.py new file mode 100644 index 00000000..93406d26 --- /dev/null +++ b/auth0/test/management/test_email_endpoints.py @@ -0,0 +1,48 @@ +import unittest +from unittest import mock + +from ...management.email_templates import EmailTemplates + + +class TestClients(unittest.TestCase): + def test_init_with_optionals(self): + t = EmailTemplates( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.email_templates.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = EmailTemplates(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/email-templates", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.email_templates.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = EmailTemplates(domain="domain", token="jwttoken") + c.get("this-template-name") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/email-templates/this-template-name" + ) + + @mock.patch("auth0.management.email_templates.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + c = EmailTemplates(domain="domain", token="jwttoken") + c.update("this-template-name", {"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/email-templates/this-template-name", + data={"a": "b", "c": "d"}, + ) diff --git a/auth0/test/management/test_emails.py b/auth0/test/management/test_emails.py new file mode 100644 index 00000000..00b4de51 --- /dev/null +++ b/auth0/test/management/test_emails.py @@ -0,0 +1,64 @@ +import unittest +from unittest import mock + +from ...management.emails import Emails + + +class TestEmails(unittest.TestCase): + def test_init_with_optionals(self): + t = Emails(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.emails.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + e = Emails(domain="domain", token="jwttoken") + e.get() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/emails/provider", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + e.get(fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/emails/provider", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + @mock.patch("auth0.management.emails.RestClient") + def test_config(self, mock_rc): + mock_instance = mock_rc.return_value + + e = Emails(domain="domain", token="jwttoken") + e.config({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/emails/provider", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.emails.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + e = Emails(domain="domain", token="jwttoken") + e.update({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/emails/provider", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.emails.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + e = Emails(domain="domain", token="jwttoken") + e.delete() + + mock_instance.delete.assert_called_with("https://domain/api/v2/emails/provider") diff --git a/auth0/test/management/test_grants.py b/auth0/test/management/test_grants.py new file mode 100644 index 00000000..e5961993 --- /dev/null +++ b/auth0/test/management/test_grants.py @@ -0,0 +1,44 @@ +import unittest +from unittest import mock + +from ...management.grants import Grants + + +class TestGrants(unittest.TestCase): + def test_init_with_optionals(self): + t = Grants(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.grants.RestClient") + def test_get_all(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Grants(domain="domain", token="jwttoken") + g.all( + extra_params={"user_id": "an-id", "client_id": "an-id", "audience": "test"} + ) + + args, kwargs = mock_instance.get.call_args + + mock_instance.get.assert_called_with( + "https://domain/api/v2/grants", + params={ + "user_id": "an-id", + "client_id": "an-id", + "audience": "test", + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + @mock.patch("auth0.management.grants.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Grants(domain="domain", token="jwttoken") + c.delete("an-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/grants/an-id") diff --git a/auth0/test/management/test_guardian.py b/auth0/test/management/test_guardian.py new file mode 100644 index 00000000..f77593e6 --- /dev/null +++ b/auth0/test/management/test_guardian.py @@ -0,0 +1,150 @@ +import unittest +from unittest import mock + +from ...management.guardian import Guardian + + +class TestGuardian(unittest.TestCase): + def test_init_with_optionals(self): + t = Guardian( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.guardian.RestClient") + def test_all_factors(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.all_factors() + + mock_instance.get.assert_called_with("https://domain/api/v2/guardian/factors") + + @mock.patch("auth0.management.guardian.RestClient") + def test_update_factor(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.update_factor("push-notification", {"enabled": True}) + + args, kwargs = mock_instance.put.call_args + self.assertEqual( + "https://domain/api/v2/guardian/factors/push-notification", args[0] + ) + self.assertEqual(kwargs["data"], {"enabled": True}) + + g.update_factor("sms", {"enabled": False}) + + args, kwargs = mock_instance.put.call_args + self.assertEqual("https://domain/api/v2/guardian/factors/sms", args[0]) + self.assertEqual(kwargs["data"], {"enabled": False}) + + @mock.patch("auth0.management.guardian.RestClient") + def test_update_templates(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.update_templates( + {"enrollment_message": "hello", "verification_message": "verified"} + ) + + args, kwargs = mock_instance.put.call_args + self.assertEqual( + "https://domain/api/v2/guardian/factors/sms/templates", args[0] + ) + self.assertEqual( + kwargs["data"], + {"enrollment_message": "hello", "verification_message": "verified"}, + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_get_templates(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.get_templates() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/guardian/factors/sms/templates" + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_get_enrollment(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.get_enrollment("some_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/guardian/enrollments/some_id" + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_delete_enrollment(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.delete_enrollment("some_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/guardian/enrollments/some_id" + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_create_enrollment_ticket(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.create_enrollment_ticket( + {"user_id": "some_id", "email": "test@test.com", "send_mail": "false"} + ) + + args, kwargs = mock_instance.post.call_args + self.assertEqual("https://domain/api/v2/guardian/enrollments/ticket", args[0]) + self.assertEqual( + kwargs["data"], + {"user_id": "some_id", "email": "test@test.com", "send_mail": "false"}, + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_get_factor_providers(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.get_factor_providers("sms", "twilio") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/guardian/factors/sms/providers/twilio" + ) + + @mock.patch("auth0.management.guardian.RestClient") + def test_update_factor_providers(self, mock_rc): + mock_instance = mock_rc.return_value + + g = Guardian(domain="domain", token="jwttoken") + g.update_factor_providers( + "sms", + "twilio", + { + "from": "test@test.com", + "messaging_service_sid": "qwerty", + "auth_token": "abc.xyz.123", + "sid": "abc.xyz", + }, + ) + + args, kwargs = mock_instance.put.call_args + self.assertEqual( + "https://domain/api/v2/guardian/factors/sms/providers/twilio", args[0] + ) + self.assertEqual( + kwargs["data"], + { + "from": "test@test.com", + "messaging_service_sid": "qwerty", + "auth_token": "abc.xyz.123", + "sid": "abc.xyz", + }, + ) diff --git a/auth0/test/management/test_hooks.py b/auth0/test/management/test_hooks.py new file mode 100644 index 00000000..b7603434 --- /dev/null +++ b/auth0/test/management/test_hooks.py @@ -0,0 +1,173 @@ +import unittest +from unittest import mock + +from ...management.hooks import Hooks + + +class TestRules(unittest.TestCase): + def test_init_with_optionals(self): + t = Hooks(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.hooks.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + + # with default params + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks", args[0]) + self.assertEqual( + kwargs["params"], + { + "enabled": "true", + "fields": None, + "include_fields": "true", + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + # with fields params + c.all(enabled=False, fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": "a,b", + "include_fields": "false", + "enabled": "false", + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + # with pagination params + c.all(page=3, per_page=27, include_totals=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "enabled": "true", + "page": 3, + "per_page": 27, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.hooks.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/hooks", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.hooks.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.get("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": None}) + + c.get("an-id", fields=["a", "b"]) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b"}) + + @mock.patch("auth0.management.hooks.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.delete("an-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/hooks/an-id") + + @mock.patch("auth0.management.hooks.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.update("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + # test for hooks secrets + @mock.patch("auth0.management.hooks.RestClient") + def test_add_secret(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.add_secrets("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id/secrets", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.hooks.RestClient") + def test_get_secrets(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.get_secrets("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id/secrets", args[0]) + self.assertNotIn("params", kwargs) + + @mock.patch("auth0.management.hooks.RestClient") + def test_delete_secrets(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.delete_secrets("an-id", ["a", "b"]) + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id/secrets", args[0]) + self.assertEqual(kwargs["data"], ["a", "b"]) + + @mock.patch("auth0.management.hooks.RestClient") + def test_update_secrets(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Hooks(domain="domain", token="jwttoken") + c.update_secrets("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/hooks/an-id/secrets", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) diff --git a/auth0/test/management/test_jobs.py b/auth0/test/management/test_jobs.py new file mode 100644 index 00000000..57313ab0 --- /dev/null +++ b/auth0/test/management/test_jobs.py @@ -0,0 +1,107 @@ +import unittest +from unittest import mock + +from ...management.jobs import Jobs + + +class TestJobs(unittest.TestCase): + def test_init_with_optionals(self): + t = Jobs(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.jobs.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + j = Jobs(domain="domain", token="jwttoken") + j.get("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/jobs/an-id", + ) + + @mock.patch("auth0.management.jobs.RestClient") + def test_get_failed_job(self, mock_rc): + mock_instance = mock_rc.return_value + + j = Jobs(domain="domain", token="jwttoken") + j.get_failed_job("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/jobs/an-id/errors", + ) + + @mock.patch("auth0.management.jobs.RestClient") + def test_export_users(self, mock_rc): + mock_instance = mock_rc.return_value + + j = Jobs(domain="domain", token="jwttoken") + j.export_users({"connection_id": "cxn_id", "format": "json"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/jobs/users-exports", + data={"connection_id": "cxn_id", "format": "json"}, + ) + + @mock.patch("auth0.management.jobs.RestClient") + def test_import_users(self, mock_rc): + mock_instance = mock_rc.return_value + + j = Jobs(domain="domain", token="jwttoken") + j.import_users(connection_id="1234", file_obj={}) + + mock_instance.file_post.assert_called_with( + "https://domain/api/v2/jobs/users-imports", + data={ + "connection_id": "1234", + "upsert": "false", + "send_completion_email": "true", + "external_id": None, + }, + files={"users": {}}, + ) + + j.import_users( + connection_id="1234", + file_obj={}, + upsert=True, + send_completion_email=False, + external_id="ext-id-123", + ) + mock_instance.file_post.assert_called_with( + "https://domain/api/v2/jobs/users-imports", + data={ + "connection_id": "1234", + "upsert": "true", + "send_completion_email": "false", + "external_id": "ext-id-123", + }, + files={"users": {}}, + ) + + j.import_users( + connection_id="1234", file_obj={}, upsert=False, send_completion_email=True + ) + mock_instance.file_post.assert_called_with( + "https://domain/api/v2/jobs/users-imports", + data={ + "connection_id": "1234", + "upsert": "false", + "send_completion_email": "true", + "external_id": None, + }, + files={"users": {}}, + ) + + @mock.patch("auth0.management.jobs.RestClient") + def test_verification_email(self, mock_rc): + mock_instance = mock_rc.return_value + + j = Jobs(domain="domain", token="jwttoken") + j.send_verification_email({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/jobs/verification-email", data={"a": "b", "c": "d"} + ) diff --git a/auth0/test/management/test_log_streams.py b/auth0/test/management/test_log_streams.py new file mode 100644 index 00000000..d81e577e --- /dev/null +++ b/auth0/test/management/test_log_streams.py @@ -0,0 +1,84 @@ +import unittest +from unittest import mock + +from ...management.log_streams import LogStreams + + +class TestLogStreams(unittest.TestCase): + def test_init_with_optionals(self): + t = LogStreams( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.log_streams.RestClient") + def test_list(self, mock_rc): + mock_instance = mock_rc.return_value + + c = LogStreams(domain="domain", token="jwttoken") + + c.list() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/log-streams", args[0]) + + @mock.patch("auth0.management.log_streams.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = LogStreams(domain="domain", token="jwttoken") + c.get("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/log-streams/an-id", args[0]) + + @mock.patch("auth0.management.log_streams.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = LogStreams(domain="domain", token="jwttoken") + # Sample data belongs to an `http` stream + log_stream_data = { + "name": "string", + "type": "http", + "sink": { + "httpEndpoint": "string", + "httpContentType": "string", + "httpContentFormat": "JSONLINES|JSONARRAY", + "httpAuthorization": "string", + }, + } + c.create(log_stream_data) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/log-streams", args[0]) + self.assertEqual(kwargs["data"], log_stream_data) + + @mock.patch("auth0.management.log_streams.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = LogStreams(domain="domain", token="jwttoken") + c.delete("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/log-streams/an-id" + ) + + @mock.patch("auth0.management.log_streams.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + log_stream_update = {"name": "string"} + + c = LogStreams(domain="domain", token="jwttoken") + c.update("an-id", log_stream_update) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/log-streams/an-id", args[0]) + self.assertEqual(kwargs["data"], log_stream_update) diff --git a/auth0/test/management/test_logs.py b/auth0/test/management/test_logs.py new file mode 100644 index 00000000..74767a6e --- /dev/null +++ b/auth0/test/management/test_logs.py @@ -0,0 +1,51 @@ +import unittest +from unittest import mock + +from ...management.logs import Logs + + +class TestLogs(unittest.TestCase): + def test_init_with_optionals(self): + t = Logs(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.logs.RestClient") + def test_search(self, mock_rc): + mock_instance = mock_rc.return_value + + logs = Logs(domain="domain", token="jwttoken") + logs.search() + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/logs", args[0]) + self.assertIsNone(kwargs["params"]["sort"]) + self.assertIsNone(kwargs["params"]["q"]) + self.assertIsNone(kwargs["params"]["from"]) + self.assertIsNone(kwargs["params"]["take"]) + self.assertEqual(kwargs["params"]["include_fields"], "true") + self.assertEqual(kwargs["params"]["include_totals"], "true") + self.assertEqual(kwargs["params"]["per_page"], 50) + self.assertEqual(kwargs["params"]["page"], 0) + self.assertIsNone(kwargs["params"]["fields"]) + + logs.search(fields=["description", "client_id"]) + + args, kwargs = mock_instance.get.call_args + self.assertEqual(kwargs["params"]["fields"], "description,client_id") + + logs.search(page=0, per_page=2) + + args, kwargs = mock_instance.get.call_args + self.assertEqual(kwargs["params"]["per_page"], 2) + self.assertEqual(kwargs["params"]["page"], 0) + + @mock.patch("auth0.management.logs.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + logs = Logs(domain="domain", token="jwttoken") + logs.get("get_id") + + mock_instance.get.assert_called_with("https://domain/api/v2/logs/get_id") diff --git a/auth0/test/management/test_network_acls.py b/auth0/test/management/test_network_acls.py new file mode 100644 index 00000000..85c80da2 --- /dev/null +++ b/auth0/test/management/test_network_acls.py @@ -0,0 +1,90 @@ +import unittest +from unittest import mock + +from ...management.network_acls import NetworkAcls + + +class TestNetworkAcls(unittest.TestCase): + def test_init_with_optionals(self): + t = NetworkAcls( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.all() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/network-acls", + params={"page": 0, "per_page": 25, "include_totals": "true"}, + ) + + s.all(page=1, per_page=50, include_totals=False) + + mock_instance.get.assert_called_with( + "https://domain/api/v2/network-acls", + params={"page": 1, "per_page": 50, "include_totals": "false"}, + ) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.create({"name": "test"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/network-acls", data={"name": "test"} + ) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.get("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/network-acls/an-id" + ) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.delete("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/network-acls/an-id" + ) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.update("an-id", {"a": "b", "c": "d"}) + + mock_instance.put.assert_called_with( + "https://domain/api/v2/network-acls/an-id", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.network_acls.RestClient") + def test_update_partial(self, mock_rc): + mock_instance = mock_rc.return_value + + s = NetworkAcls(domain="domain", token="jwttoken") + s.update_partial("an-id", {"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/network-acls/an-id", + data={"a": "b", "c": "d"}, + ) \ No newline at end of file diff --git a/auth0/test/management/test_organizations.py b/auth0/test/management/test_organizations.py new file mode 100644 index 00000000..6b1967b9 --- /dev/null +++ b/auth0/test/management/test_organizations.py @@ -0,0 +1,536 @@ +import unittest +from unittest import mock + +from ...management.organizations import Organizations + + +class TestOrganizations(unittest.TestCase): + def test_init_with_optionals(self): + t = Organizations( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + # Organizations + @mock.patch("auth0.management.organizations.RestClient") + def test_all_organizations(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all_organizations() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/organizations", args[0]) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "true", + "from": None, + "take": None, + }, + ) + + # Basic pagination + c.all_organizations(page=7, per_page=25, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/organizations", args[0]) + self.assertEqual( + kwargs["params"], + { + "page": 7, + "per_page": 25, + "include_totals": "false", + "from": None, + "take": None, + }, + ) + + # Checkpoint pagination + c.all_organizations(from_param=8675309, take=75) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/organizations", args[0]) + self.assertEqual( + kwargs["params"], + { + "from": 8675309, + "take": 75, + "page": None, + "per_page": None, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_organization_by_name(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_organization_by_name("test-org") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/organizations/name/test-org", args[0]) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_organization(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_organization("org123") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/organizations/org123", args[0]) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_create_organization(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.create_organization({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations", data={"a": "b", "c": "d"} + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_update_organization(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.update_organization("this-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/organizations/this-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_organization(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_organization("this-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/this-id" + ) + + # Organization Connections + @mock.patch("auth0.management.organizations.RestClient") + def test_all_organization_connections(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all_organization_connections("test-org") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/enabled_connections", args[0] + ) + self.assertEqual(kwargs["params"], {"page": None, "per_page": None}) + + # Specific pagination + c.all_organization_connections("test-org", page=7, per_page=25) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/enabled_connections", args[0] + ) + self.assertEqual(kwargs["params"], {"page": 7, "per_page": 25}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_organization_connection(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_organization_connection("test-org", "test-con") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/enabled_connections/test-con", + args[0], + ) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_create_organization_connection(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.create_organization_connection("test-org", {"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/enabled_connections", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_update_organization_connection(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.update_organization_connection("test-org", "test-con", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/enabled_connections/test-con", + args[0], + ) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_organization_connection(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_organization_connection("test-org", "test-con") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/enabled_connections/test-con" + ) + + # Organization Members + @mock.patch("auth0.management.organizations.RestClient") + def test_all_organization_members(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all_organization_members("test-org") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "true", + "from": None, + "take": None, + "fields": None, + "include_fields": "true", + }, + ) + + # Specific pagination + c.all_organization_members( + "test-org", page=7, per_page=25, include_totals=False + ) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": 7, + "per_page": 25, + "include_totals": "false", + "from": None, + "take": None, + "fields": None, + "include_fields": "true", + }, + ) + + # Checkpoint pagination + c.all_organization_members("test-org", from_param=8675309, take=75) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "from": 8675309, + "take": 75, + "page": None, + "per_page": None, + "include_totals": "true", + "fields": None, + "include_fields": "true", + }, + ) + + # With fields + c.all_organization_members("test-org", fields=["a,b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "true", + "from": None, + "take": None, + "fields": "a,b", + "include_fields": "false", + }, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_create_organization_members(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.create_organization_members("test-org", {"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/members", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_organization_members(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_organization_members("test-org", {"a": "b", "c": "d"}) + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/members", + data={"a": "b", "c": "d"}, + ) + + # Organization Member Roles + @mock.patch("auth0.management.organizations.RestClient") + def test_all_organization_member_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all_organization_member_roles("test-org", "test-user") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members/test-user/roles", + args[0], + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "false", + } + ) + + # Specific pagination + c.all_organization_member_roles("test-org", "test-user", page=7, per_page=25, include_totals=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/members/test-user/roles", + args[0], + ) + self.assertEqual( + kwargs["params"], + { + "page": 7, + "per_page": 25, + "include_totals": "true", + } + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_create_organization_member_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.create_organization_member_roles( + "test-org", "test-user", {"a": "b", "c": "d"} + ) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/members/test-user/roles", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_organization_member_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_organization_member_roles( + "test-org", "test-user", {"a": "b", "c": "d"} + ) + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/members/test-user/roles", + data={"a": "b", "c": "d"}, + ) + + # Organization Invitations + @mock.patch("auth0.management.organizations.RestClient") + def test_all_organization_invitations(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + + # Default parameters are requested + c.all_organization_invitations("test-org") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/invitations", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + # Specific pagination + c.all_organization_invitations("test-org", page=7, per_page=25) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/invitations", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": 7, + "per_page": 25, + "include_totals": "false", + }, + ) + + # Return paged collection with paging properties + c.all_organization_invitations( + "test-org", page=7, per_page=25, include_totals=True + ) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/invitations", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": 7, + "per_page": 25, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_organization_invitation(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_organization_invitation("test-org", "test-inv") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/organizations/test-org/invitations/test-inv", args[0] + ) + self.assertEqual(kwargs["params"], {}) + + @mock.patch("auth0.management.organizations.RestClient") + def test_create_organization_invitation(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.create_organization_invitation("test-org", {"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/invitations", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_organization_invitation(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_organization_invitation("test-org", "test-inv") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/invitations/test-inv" + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_client_grants(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_client_grants("test-org") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants", + params={ + "audience": None, + "client_id": None, + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_add_client_grant(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.add_client_grant("test-org", "test-cg") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants", + data={"grant_id": "test-cg"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_client_grant(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_client_grant("test-org", "test-cg") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants/test-cg", + ) diff --git a/auth0/v3/test/management/test_prompts.py b/auth0/test/management/test_prompts.py similarity index 75% rename from auth0/v3/test/management/test_prompts.py rename to auth0/test/management/test_prompts.py index 06423ebb..a93f2faa 100644 --- a/auth0/v3/test/management/test_prompts.py +++ b/auth0/test/management/test_prompts.py @@ -1,6 +1,5 @@ import unittest - -import mock +from unittest import mock from ...management.prompts import Prompts @@ -12,7 +11,7 @@ def test_init_with_optionals(self): telemetry_header = t.client.base_headers.get("Auth0-Client", None) self.assertEqual(telemetry_header, None) - @mock.patch("auth0.v3.management.prompts.RestClient") + @mock.patch("auth0.management.prompts.RestClient") def test_get(self, mock_rc): mock_instance = mock_rc.return_value @@ -23,7 +22,7 @@ def test_get(self, mock_rc): self.assertEqual("https://domain/api/v2/prompts", args[0]) - @mock.patch("auth0.v3.management.prompts.RestClient") + @mock.patch("auth0.management.prompts.RestClient") def test_update(self, mock_rc): mock_instance = mock_rc.return_value @@ -35,7 +34,7 @@ def test_update(self, mock_rc): self.assertEqual("https://domain/api/v2/prompts", args[0]) self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) - @mock.patch("auth0.v3.management.prompts.RestClient") + @mock.patch("auth0.management.prompts.RestClient") def test_get_custom_text(self, mock_rc): mock_instance = mock_rc.return_value @@ -44,9 +43,12 @@ def test_get_custom_text(self, mock_rc): args, _ = mock_instance.get.call_args - self.assertEqual("https://domain/api/v2/prompts/some-prompt/custom-text/some-language", args[0]) + self.assertEqual( + "https://domain/api/v2/prompts/some-prompt/custom-text/some-language", + args[0], + ) - @mock.patch("auth0.v3.management.prompts.RestClient") + @mock.patch("auth0.management.prompts.RestClient") def test_update_custom_text(self, mock_rc): mock_instance = mock_rc.return_value @@ -55,5 +57,8 @@ def test_update_custom_text(self, mock_rc): args, kwargs = mock_instance.put.call_args - self.assertEqual("https://domain/api/v2/prompts/some-prompt/custom-text/some-language", args[0]) + self.assertEqual( + "https://domain/api/v2/prompts/some-prompt/custom-text/some-language", + args[0], + ) self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) diff --git a/auth0/test/management/test_resource_servers.py b/auth0/test/management/test_resource_servers.py new file mode 100644 index 00000000..878b02d7 --- /dev/null +++ b/auth0/test/management/test_resource_servers.py @@ -0,0 +1,86 @@ +import unittest +from unittest import mock + +from ...management.resource_servers import ResourceServers + + +class TestResourceServers(unittest.TestCase): + def test_init_with_optionals(self): + t = ResourceServers( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.resource_servers.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + r = ResourceServers(domain="domain", token="jwttoken") + + r.create({"name": "TestApi", "identifier": "https://test.com/api"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/resource-servers", + data={"name": "TestApi", "identifier": "https://test.com/api"}, + ) + + @mock.patch("auth0.management.resource_servers.RestClient") + def test_get_all(self, mock_rc): + mock_instance = mock_rc.return_value + + r = ResourceServers(domain="domain", token="jwttoken") + + # with default params + r.get_all() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/resource-servers", + params={"page": None, "per_page": None, "include_totals": "false"}, + ) + + # with pagination params + r.get_all(page=3, per_page=27, include_totals=True) + + mock_instance.get.assert_called_with( + "https://domain/api/v2/resource-servers", + params={"page": 3, "per_page": 27, "include_totals": "true"}, + ) + + @mock.patch("auth0.management.resource_servers.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + r = ResourceServers(domain="domain", token="jwttoken") + + r.get("some_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/resource-servers/some_id" + ) + + @mock.patch("auth0.management.resource_servers.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + r = ResourceServers(domain="domain", token="jwttoken") + + r.delete("some_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/resource-servers/some_id" + ) + + @mock.patch("auth0.management.resource_servers.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + r = ResourceServers(domain="domain", token="jwttoken") + + r.update("some_id", {"name": "TestApi2", "identifier": "https://test.com/api2"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/resource-servers/some_id", + data={"name": "TestApi2", "identifier": "https://test.com/api2"}, + ) diff --git a/auth0/test/management/test_rest.py b/auth0/test/management/test_rest.py new file mode 100644 index 00000000..6288daf9 --- /dev/null +++ b/auth0/test/management/test_rest.py @@ -0,0 +1,831 @@ +import base64 +import json +import sys +import unittest +from unittest import mock + +from auth0.rest import RestClient, RestClientOptions + +from ...exceptions import Auth0Error, RateLimitError + + +class TestRest(unittest.TestCase): + # def test_options_are_used_and_override(self): + # """ + # This test ensures RestClientOptions are read when passed to + # RestClient's constructor by (1) configuring a timeout and (2) + # turning off Telemetry. This proves that RestClient is inheriting + # those options, and overriding it's own constructor arguments. + # """ + + # options = RestClientOptions(telemetry=False, timeout=0.00002, retries=10) + # rc = RestClient(jwt="a-token", telemetry=True, timeout=30, options=options) + + # # Does a timeout occur as expected? + # with self.assertRaises(requests.exceptions.Timeout): + # rc.get("http://google.com") + + # # Is RestClient using the RestClientOptions.timeout value properly? + # self.assertEqual(rc.options.timeout, 0.00002) + + # # Is RestClient using the RestClientOptions.retries value properly? + # self.assertEqual(rc.options.retries, 10) + + # # Is RestClient using the RestClientOptions.telemetry value properly? + # self.assertEqual(rc.options.telemetry, False) + + # # Is RestClient using the RestClientOptions.telemetry value properly? + # self.assertEqual( + # rc.base_headers, + # { + # "Content-Type": "application/json", + # "Authorization": "Bearer a-token", + # }, + # ) + + # def test_options_are_created_by_default(self): + # """ + # This test ensures RestClientOptions are read when passed to + # RestClient's constructor by (1) configuring a timeout and (2) + # turning off Telemetry. This proves that RestClient is inheriting + # those options, and overriding it's own constructor arguments. + # """ + + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # # Does a timeout occur as expected? + # with self.assertRaises(requests.exceptions.Timeout): + # rc.get("http://google.com") + + # # Did RestClient create a RestClientOptions for us? + # self.assertIsNotNone(rc.options) + + # # Did RestClient assign the new RestClientOptions instance the proper timeout value from the constructor? + # self.assertEqual(rc.options.timeout, 0.00002) + + # # Did RestClient use the default RestClientOptions value for retries? + # self.assertEqual(rc.options.retries, 3) + + # # Did RestClient assign the new RestClientOptions instance the proper telemetry value from the constructor? + # self.assertEqual(rc.options.telemetry, False) + + # # Is RestClient using the RestClientOptions.telemetry value properly? + # self.assertEqual( + # rc.base_headers, + # { + # "Content-Type": "application/json", + # "Authorization": "Bearer a-token", + # }, + # ) + + def test_default_options_are_used(self): + """ + This test ensures RestClientOptions are read when passed to + RestClient's constructor by (1) configuring a timeout and (2) + turning off Telemetry. This proves that RestClient is inheriting + those options, and overriding it's own constructor arguments. + """ + + options = RestClientOptions() + rc = RestClient(jwt="a-token", options=options) + + # Did RestClient store the RestClientOptions? + self.assertIsNotNone(rc.options) + + # Did RestClientOptions use the default 5.0 timeout? + self.assertEqual(rc.options.timeout, 5.0) + + # Did RestClientOptions use the default 3 retries? + self.assertEqual(rc.options.retries, 3) + + # Did RestClientOptions use the default True telemetry value? + self.assertEqual(rc.options.telemetry, True) + + # TODO: Replace the following with more reliable tests. Failing on GitHub Actions. + + # def test_get_can_timeout(self): + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # rc.get("https://google.com") + + # def test_post_can_timeout(self): + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # rc.post("https://google.com") + + # def test_put_can_timeout(self): + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # rc.put("https://google.com") + + # def test_patch_can_timeout(self): + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # rc.patch("https://google.com") + + # def test_delete_can_timeout(self): + # rc = RestClient(jwt="a-token", telemetry=False, timeout=0.00002) + + # with self.assertRaises(requests.exceptions.Timeout): + # rc.delete("https://google.com") + + @mock.patch("requests.request") + def test_get_custom_timeout(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False, timeout=(10, 2)) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + rc.get("the-url") + mock_request.assert_called_with( + "GET", "the-url", headers=headers, timeout=(10, 2) + ) + + @mock.patch("requests.request") + def test_post_custom_timeout(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False, timeout=(10, 2)) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + rc.post("the-url") + mock_request.assert_called_with( + "POST", "the-url", headers=headers, timeout=(10, 2) + ) + + @mock.patch("requests.request") + def test_put_custom_timeout(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False, timeout=(10, 2)) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + rc.put("the-url") + mock_request.assert_called_with( + "PUT", "the-url", headers=headers, timeout=(10, 2) + ) + + @mock.patch("requests.request") + def test_patch_custom_timeout(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False, timeout=(10, 2)) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + rc.patch("the-url") + mock_request.assert_called_with( + "PATCH", "the-url", headers=headers, timeout=(10, 2) + ) + + @mock.patch("requests.request") + def test_delete_custom_timeout(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False, timeout=(10, 2)) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + rc.delete("the-url") + mock_request.assert_called_with( + "DELETE", "the-url", headers=headers, timeout=(10, 2) + ) + + @mock.patch("requests.request") + def test_get(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + response = rc.get("the-url") + mock_request.assert_called_with("GET", "the-url", headers=headers, timeout=5.0) + + self.assertEqual(response, ["a", "b"]) + + response = rc.get(url="the/url", params={"A": "param", "B": "param"}) + mock_request.assert_called_with( + "GET", + "the/url", + params={"A": "param", "B": "param"}, + headers=headers, + timeout=5.0, + ) + self.assertEqual(response, ["a", "b"]) + + mock_request.return_value.text = "" + response = rc.get("the/url") + self.assertEqual(response, "") + + @mock.patch("requests.request") + def test_get_errors(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 999, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + @mock.patch("requests.request") + def test_get_rate_limit_error(self, mock_request): + options = RestClientOptions(telemetry=False, retries=0) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + self.assertIsNotNone(context.exception.headers) + self.assertEqual(context.exception.headers["x-ratelimit-limit"], "3") + self.assertEqual(context.exception.headers["x-ratelimit-remaining"], "6") + self.assertEqual(context.exception.headers["x-ratelimit-reset"], "9") + + self.assertEqual(rc._metrics["retries"], 0) + + @mock.patch("requests.request") + def test_get_rate_limit_error_without_headers(self, mock_request): + options = RestClientOptions(telemetry=False, retries=1) + rc = RestClient(jwt="a-token", options=options) + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + + mock_request.return_value.headers = {} + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, -1) + self.assertIsNotNone(context.exception.headers) + self.assertEqual(context.exception.headers, {}) + + self.assertEqual(rc._metrics["retries"], 1) + + @mock.patch("requests.request") + def test_get_rate_limit_custom_retries(self, mock_request): + options = RestClientOptions(telemetry=False, retries=5) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + + self.assertEqual(rc._metrics["retries"], 5) + self.assertEqual(rc._metrics["retries"], len(rc._metrics["backoff"])) + + @mock.patch("requests.request") + def test_get_rate_limit_invalid_retries_below_min(self, mock_request): + options = RestClientOptions(telemetry=False, retries=-1) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + + self.assertEqual(rc._metrics["retries"], 0) + + @mock.patch("requests.request") + def test_get_rate_limit_invalid_retries_above_max(self, mock_request): + options = RestClientOptions(telemetry=False, retries=11) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + + self.assertEqual(rc._metrics["retries"], rc.MAX_REQUEST_RETRIES()) + + @mock.patch("requests.request") + def test_get_rate_limit_retries_use_exponential_backoff(self, mock_request): + options = RestClientOptions(telemetry=False, retries=10) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + mock_request.return_value.headers = { + "x-ratelimit-limit": "3", + "x-ratelimit-remaining": "6", + "x-ratelimit-reset": "9", + } + + with self.assertRaises(Auth0Error) as context: + rc.get("the/url") + + self.assertEqual(context.exception.status_code, 429) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + self.assertIsInstance(context.exception, RateLimitError) + self.assertEqual(context.exception.reset_at, 9) + + self.assertEqual(rc._metrics["retries"], 10) + self.assertEqual(rc._metrics["retries"], len(rc._metrics["backoff"])) + + baseBackoff = [0] + baseBackoffSum = 0 + finalBackoff = 0 + + for i in range(0, 9): + backoff = 100 * 2**i + baseBackoff.append(backoff) + baseBackoffSum += backoff + + for backoff in rc._metrics["backoff"]: + finalBackoff += backoff + + # Assert that exponential backoff is happening. + self.assertGreaterEqual(rc._metrics["backoff"][1], rc._metrics["backoff"][0]) + self.assertGreaterEqual(rc._metrics["backoff"][2], rc._metrics["backoff"][1]) + self.assertGreaterEqual(rc._metrics["backoff"][3], rc._metrics["backoff"][2]) + self.assertGreaterEqual(rc._metrics["backoff"][4], rc._metrics["backoff"][3]) + self.assertGreaterEqual(rc._metrics["backoff"][5], rc._metrics["backoff"][4]) + self.assertGreaterEqual(rc._metrics["backoff"][6], rc._metrics["backoff"][5]) + self.assertGreaterEqual(rc._metrics["backoff"][7], rc._metrics["backoff"][6]) + self.assertGreaterEqual(rc._metrics["backoff"][8], rc._metrics["backoff"][7]) + self.assertGreaterEqual(rc._metrics["backoff"][9], rc._metrics["backoff"][8]) + + # Ensure jitter is being applied. + self.assertNotEqual(rc._metrics["backoff"][1], baseBackoff[1]) + self.assertNotEqual(rc._metrics["backoff"][2], baseBackoff[2]) + self.assertNotEqual(rc._metrics["backoff"][3], baseBackoff[3]) + self.assertNotEqual(rc._metrics["backoff"][4], baseBackoff[4]) + self.assertNotEqual(rc._metrics["backoff"][5], baseBackoff[5]) + self.assertNotEqual(rc._metrics["backoff"][6], baseBackoff[6]) + self.assertNotEqual(rc._metrics["backoff"][7], baseBackoff[7]) + self.assertNotEqual(rc._metrics["backoff"][8], baseBackoff[8]) + self.assertNotEqual(rc._metrics["backoff"][9], baseBackoff[9]) + + # Ensure subsequent delay is never less than the minimum. + self.assertGreaterEqual(rc._metrics["backoff"][1], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][2], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][3], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][4], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][5], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][6], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][7], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][8], rc.MIN_REQUEST_RETRY_DELAY()) + self.assertGreaterEqual(rc._metrics["backoff"][9], rc.MIN_REQUEST_RETRY_DELAY()) + + # Ensure delay is never more than the maximum. + self.assertLessEqual(rc._metrics["backoff"][0], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][1], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][2], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][3], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][4], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][5], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][6], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][7], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][8], rc.MAX_REQUEST_RETRY_DELAY()) + self.assertLessEqual(rc._metrics["backoff"][9], rc.MAX_REQUEST_RETRY_DELAY()) + + # Ensure total delay sum is never more than 10s. + self.assertLessEqual(finalBackoff, 10000) + + @mock.patch("requests.request") + def test_post_rate_limit_retries(self, mock_request): + options = RestClientOptions(telemetry=False, retries=10) + rc = RestClient(jwt="a-token", options=options) + rc._skip_sleep = True + + mock_request.return_value.text = ( + '{"statusCode": 429, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 429 + + with self.assertRaises(Auth0Error) as context: + rc.post("the/url") + + self.assertEqual(context.exception.status_code, 429) + + self.assertEqual(len(rc._metrics["backoff"]), 10) + + @mock.patch("requests.request") + def test_post(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '{"a": "b"}' + + data = {"some": "data"} + + mock_request.return_value.status_code = 200 + response = rc.post("the/url", data=data) + mock_request.assert_called_with( + "POST", "the/url", json=data, headers=headers, timeout=5.0 + ) + + self.assertEqual(response, {"a": "b"}) + + @mock.patch("requests.request") + def test_post_errors(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 999, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + @mock.patch("requests.request") + def test_post_errors_with_no_message_property(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = json.dumps( + {"statusCode": 999, "errorCode": "code", "error": "error"} + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "error") + + @mock.patch("requests.request") + def test_post_errors_with_no_message_or_error_property(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = json.dumps( + {"statusCode": 999, "errorCode": "code"} + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "") + + @mock.patch("requests.request") + def test_post_errors_with_message_and_error_property(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = json.dumps( + { + "statusCode": 999, + "errorCode": "code", + "error": "error", + "message": "message", + } + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + @mock.patch("requests.request") + def test_post_error_with_code_property(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = '{"errorCode": "e0","message": "desc"}' + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "e0") + self.assertEqual(context.exception.message, "desc") + + @mock.patch("requests.request") + def test_post_error_with_no_error_code(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = '{"message": "desc"}' + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual(context.exception.message, "desc") + + @mock.patch("requests.request") + def test_post_error_with_text_response(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = "there has been a terrible error" + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual( + context.exception.message, "there has been a terrible error" + ) + + @mock.patch("requests.request") + def test_post_error_with_no_response_text(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + for error_status in [400, 500, None]: + mock_request.return_value.status_code = error_status + mock_request.return_value.text = None + + with self.assertRaises(Auth0Error) as context: + rc.post("the-url") + + self.assertEqual(context.exception.status_code, error_status) + self.assertEqual(context.exception.error_code, "a0.sdk.internal.unknown") + self.assertEqual(context.exception.message, "") + + @mock.patch("requests.request") + def test_file_post_content_type_is_none(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = {"Authorization": "Bearer a-token"} + mock_request.return_value.status_code = 200 + mock_request.return_value.text = "Success" + + data = {"some": "data"} + files = [mock.Mock()] + + rc.file_post("the-url", data=data, files=files) + + mock_request.assert_called_once_with( + "POST", "the-url", data=data, files=files, headers=headers, timeout=5.0 + ) + + @mock.patch("requests.request") + def test_put(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + data = {"some": "data"} + + response = rc.put(url="the-url", data=data) + mock_request.assert_called_with( + "PUT", "the-url", json=data, headers=headers, timeout=5.0 + ) + + self.assertEqual(response, ["a", "b"]) + + @mock.patch("requests.request") + def test_put_errors(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 999, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.put(url="the/url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + @mock.patch("requests.request") + def test_patch(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + data = {"some": "data"} + + response = rc.patch(url="the-url", data=data) + mock_request.assert_called_with( + "PATCH", "the-url", json=data, headers=headers, timeout=5.0 + ) + + self.assertEqual(response, ["a", "b"]) + + @mock.patch("requests.request") + def test_patch_errors(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 999, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.patch(url="the/url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + @mock.patch("requests.request") + def test_delete(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + response = rc.delete(url="the-url/ID") + mock_request.assert_called_with( + "DELETE", "the-url/ID", headers=headers, timeout=5.0 + ) + + self.assertEqual(response, ["a", "b"]) + + @mock.patch("requests.request") + def test_delete_with_body_and_params(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + headers = { + "Authorization": "Bearer a-token", + "Content-Type": "application/json", + } + + mock_request.return_value.text = '["a", "b"]' + mock_request.return_value.status_code = 200 + + data = {"some": "data"} + params = {"A": "param", "B": "param"} + + response = rc.delete(url="the-url/ID", params=params, data=data) + mock_request.assert_called_with( + "DELETE", + "the-url/ID", + headers=headers, + params=params, + json=data, + timeout=5.0, + ) + + self.assertEqual(response, ["a", "b"]) + + @mock.patch("requests.request") + def test_delete_errors(self, mock_request): + rc = RestClient(jwt="a-token", telemetry=False) + + mock_request.return_value.text = ( + '{"statusCode": 999, "errorCode": "code", "message": "message"}' + ) + mock_request.return_value.status_code = 999 + + with self.assertRaises(Auth0Error) as context: + rc.delete(url="the-url") + + self.assertEqual(context.exception.status_code, 999) + self.assertEqual(context.exception.error_code, "code") + self.assertEqual(context.exception.message, "message") + + def test_disabled_telemetry(self): + rc = RestClient(jwt="a-token", telemetry=False) + expected_headers = { + "Content-Type": "application/json", + "Authorization": "Bearer a-token", + } + + self.assertEqual(rc.base_headers, expected_headers) + + def test_enabled_telemetry(self): + rc = RestClient(jwt="a-token", telemetry=True) + + user_agent = rc.base_headers["User-Agent"] + auth0_client_bytes = base64.b64decode(rc.base_headers["Auth0-Client"]) + auth0_client_json = auth0_client_bytes.decode("utf-8") + auth0_client = json.loads(auth0_client_json) + content_type = rc.base_headers["Content-Type"] + + from auth0 import __version__ as auth0_version + + python_version = "{}.{}.{}".format( + sys.version_info.major, sys.version_info.minor, sys.version_info.micro + ) + + client_info = { + "name": "auth0-python", + "version": auth0_version, + "env": {"python": python_version}, + } + + self.assertEqual(user_agent, f"Python/{python_version}") + self.assertEqual(auth0_client, client_info) + self.assertEqual(content_type, "application/json") diff --git a/auth0/test/management/test_roles.py b/auth0/test/management/test_roles.py new file mode 100644 index 00000000..d8ea37a8 --- /dev/null +++ b/auth0/test/management/test_roles.py @@ -0,0 +1,196 @@ +import unittest +from unittest import mock + +from ...management.roles import Roles + + +class TestRoles(unittest.TestCase): + def test_init_with_optionals(self): + t = Roles(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.roles.RestClient") + def test_list(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.list() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles", args[0]) + self.assertEqual( + kwargs["params"], + {"per_page": 25, "page": 0, "include_totals": "true", "name_filter": None}, + ) + + u.list(page=1, per_page=50, include_totals=False, name_filter="little-role") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles", args[0]) + self.assertEqual( + kwargs["params"], + { + "per_page": 50, + "page": 1, + "include_totals": "false", + "name_filter": "little-role", + }, + ) + + @mock.patch("auth0.management.roles.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.create({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/roles", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.roles.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.get("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id", args[0]) + + @mock.patch("auth0.management.roles.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.delete("an-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/roles/an-id") + + @mock.patch("auth0.management.roles.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.update("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.roles.RestClient") + def test_list_users(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.list_users("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/roles/an-id/users", args[0]) + self.assertEqual( + kwargs["params"], + { + "per_page": 25, + "page": 0, + "include_totals": "true", + "from": None, + "take": None, + }, + ) + + u.list_users(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/users", args[0]) + self.assertEqual( + kwargs["params"], + { + "per_page": 50, + "page": 1, + "include_totals": "false", + "from": None, + "take": None, + }, + ) + + u.list_users(id="an-id", from_param=8675309, take=75) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/users", args[0]) + self.assertEqual( + kwargs["params"], + { + "from": 8675309, + "take": 75, + "per_page": 25, + "page": 0, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.roles.RestClient") + def test_add_users(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.add_users("an-id", ["a", "b"]) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/users", args[0]) + self.assertEqual(kwargs["data"], {"users": ["a", "b"]}) + + @mock.patch("auth0.management.roles.RestClient") + def test_list_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.list_permissions("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/roles/an-id/permissions", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"} + ) + + u.list_permissions(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/permissions", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"} + ) + + @mock.patch("auth0.management.roles.RestClient") + def test_remove_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.remove_permissions("an-id", ["a", "b"]) + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/permissions", args[0]) + self.assertEqual(kwargs["data"], {"permissions": ["a", "b"]}) + + @mock.patch("auth0.management.roles.RestClient") + def test_add_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Roles(domain="domain", token="jwttoken") + u.add_permissions("an-id", ["a", "b"]) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/roles/an-id/permissions", args[0]) + self.assertEqual(kwargs["data"], {"permissions": ["a", "b"]}) diff --git a/auth0/test/management/test_rules.py b/auth0/test/management/test_rules.py new file mode 100644 index 00000000..e0c2b73d --- /dev/null +++ b/auth0/test/management/test_rules.py @@ -0,0 +1,127 @@ +import unittest +from unittest import mock + +from ...management.rules import Rules + + +class TestRules(unittest.TestCase): + def test_init_with_optionals(self): + t = Rules(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.rules.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Rules(domain="domain", token="jwttoken") + + # with default params + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "enabled": "true", + "stage": "login_success", + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + # with stage and fields params + c.all(stage="stage", enabled=False, fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": "a,b", + "include_fields": "false", + "enabled": "false", + "stage": "stage", + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + # with pagination params + c.all(page=3, per_page=27, include_totals=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules", args[0]) + self.assertEqual( + kwargs["params"], + { + "fields": None, + "include_fields": "true", + "enabled": "true", + "stage": "login_success", + "page": 3, + "per_page": 27, + "include_totals": "true", + }, + ) + + @mock.patch("auth0.management.rules.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Rules(domain="domain", token="jwttoken") + c.create({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/rules", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.rules.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Rules(domain="domain", token="jwttoken") + c.get("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + c.get("an-id", fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + @mock.patch("auth0.management.rules.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Rules(domain="domain", token="jwttoken") + c.delete("an-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/rules/an-id") + + @mock.patch("auth0.management.rules.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Rules(domain="domain", token="jwttoken") + c.update("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/rules/an-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) diff --git a/auth0/test/management/test_rules_configs.py b/auth0/test/management/test_rules_configs.py new file mode 100644 index 00000000..52f1b5b8 --- /dev/null +++ b/auth0/test/management/test_rules_configs.py @@ -0,0 +1,47 @@ +import unittest +from unittest import mock + +from ...management.rules_configs import RulesConfigs + + +class TestRulesConfigs(unittest.TestCase): + def test_init_with_optionals(self): + t = RulesConfigs( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.rules_configs.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + c = RulesConfigs(domain="domain", token="jwttoken") + + c.all() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/rules-configs", args[0]) + + @mock.patch("auth0.management.rules_configs.RestClient") + def test_unset(self, mock_rc): + mock_instance = mock_rc.return_value + + c = RulesConfigs(domain="domain", token="jwttoken") + c.unset("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/rules-configs/an-id" + ) + + @mock.patch("auth0.management.rules_configs.RestClient") + def test_set(self, mock_rc): + mock_instance = mock_rc.return_value + + g = RulesConfigs(domain="domain", token="jwttoken") + g.set("key", "MY_RULES_CONFIG_VALUES") + + args, kwargs = mock_instance.put.call_args + self.assertEqual("https://domain/api/v2/rules-configs/key", args[0]) diff --git a/auth0/test/management/test_self_service_profiles.py b/auth0/test/management/test_self_service_profiles.py new file mode 100644 index 00000000..0bc6fb39 --- /dev/null +++ b/auth0/test/management/test_self_service_profiles.py @@ -0,0 +1,124 @@ +import unittest +from unittest import mock + +from ...management.self_service_profiles import SelfServiceProfiles + + +class TestSelfServiceProfiles(unittest.TestCase): + def test_init_with_optionals(self): + t = SelfServiceProfiles( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.all() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles", + params={"page": 0, "per_page": 25, "include_totals": "true"}, + ) + + s.all(page=1, per_page=50, include_totals=False) + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles", + params={"page": 1, "per_page": 50, "include_totals": "false"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.create({"name": "test"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles", data={"name": "test"} + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.get("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.delete("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.update("an-id", {"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_get_custom_text(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.get_custom_text("an-id", "en", "page") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_update_custom_text(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.update_custom_text("an-id", "en", "page", {"a": "b", "c": "d"}) + + mock_instance.put.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_create_sso_ticket(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.create_sso_ticket("an-id", {"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/sso-ticket", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_revoke_sso_ticket(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.revoke_sso_ticket("an-id", "ticket-id") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/sso-ticket/ticket-id/revoke" + ) \ No newline at end of file diff --git a/auth0/test/management/test_stats.py b/auth0/test/management/test_stats.py new file mode 100644 index 00000000..b8ec01c0 --- /dev/null +++ b/auth0/test/management/test_stats.py @@ -0,0 +1,42 @@ +import unittest +from unittest import mock + +from ...management.stats import Stats + + +class TestStats(unittest.TestCase): + def test_init_with_optionals(self): + t = Stats(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.stats.RestClient") + def test_active_users(self, mock_rc): + mock_instance = mock_rc.return_value + + s = Stats(domain="domain", token="jwttoken") + s.active_users() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/stats/active-users", + ) + + @mock.patch("auth0.management.stats.RestClient") + def test_daily_stats(self, mock_rc): + mock_instance = mock_rc.return_value + + s = Stats(domain="domain", token="jwttoken") + s.daily_stats() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/stats/daily", + params={"from": None, "to": None}, + ) + + s.daily_stats(from_date="12341212", to_date="56785656") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/stats/daily", + params={"from": "12341212", "to": "56785656"}, + ) diff --git a/auth0/test/management/test_tenants.py b/auth0/test/management/test_tenants.py new file mode 100644 index 00000000..41a03fdd --- /dev/null +++ b/auth0/test/management/test_tenants.py @@ -0,0 +1,51 @@ +import unittest +from unittest import mock + +from ...management.tenants import Tenants + + +class TestTenants(unittest.TestCase): + def test_init_with_optionals(self): + t = Tenants(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.tenants.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.get.return_value = {} + + t = Tenants(domain="domain", token="jwttoken") + t.get() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/tenants/settings", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + t.get(fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/tenants/settings", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + t.get(fields=["a", "b"], include_fields=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/tenants/settings", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "true"}) + + @mock.patch("auth0.management.tenants.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + mock_instance.patch.return_value = {} + + t = Tenants(domain="domain", token="jwttoken") + t.update({"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/tenants/settings", data={"a": "b", "c": "d"} + ) diff --git a/auth0/test/management/test_tickets.py b/auth0/test/management/test_tickets.py new file mode 100644 index 00000000..3b854793 --- /dev/null +++ b/auth0/test/management/test_tickets.py @@ -0,0 +1,35 @@ +import unittest +from unittest import mock + +from ...management.tickets import Tickets + + +class TestTickets(unittest.TestCase): + def test_init_with_optionals(self): + t = Tickets(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.tickets.RestClient") + def test_email(self, mock_rc): + mock_instance = mock_rc.return_value + + t = Tickets(domain="domain", token="jwttoken") + t.create_email_verification({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/tickets/email-verification", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.tickets.RestClient") + def test_pswd(self, mock_rc): + mock_instance = mock_rc.return_value + + t = Tickets(domain="domain", token="jwttoken") + t.create_pswd_change({"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/tickets/password-change", data={"a": "b", "c": "d"} + ) diff --git a/auth0/test/management/test_user_blocks.py b/auth0/test/management/test_user_blocks.py new file mode 100644 index 00000000..9057310e --- /dev/null +++ b/auth0/test/management/test_user_blocks.py @@ -0,0 +1,63 @@ +import unittest +from unittest import mock + +from ...management.user_blocks import UserBlocks + + +class TestUserBlocks(unittest.TestCase): + def test_init_with_optionals(self): + t = UserBlocks( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.user_blocks.RestClient") + def test_get_by_identifier(self, mock_rc): + mock_instance = mock_rc.return_value + + u = UserBlocks(domain="domain", token="jwttoken") + + u.get_by_identifier("some_identifier") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/user-blocks", + params={"identifier": "some_identifier"}, + ) + + @mock.patch("auth0.management.user_blocks.RestClient") + def test_unblock_by_identifier(self, mock_rc): + mock_instance = mock_rc.return_value + + u = UserBlocks(domain="domain", token="jwttoken") + + u.unblock_by_identifier("test@test.com") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/user-blocks", params={"identifier": "test@test.com"} + ) + + @mock.patch("auth0.management.user_blocks.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + u = UserBlocks(domain="domain", token="jwttoken") + + u.get("auth0|584ad3c228be27504a2c80d5") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/user-blocks/auth0|584ad3c228be27504a2c80d5" + ) + + @mock.patch("auth0.management.user_blocks.RestClient") + def test_unblock(self, mock_rc): + mock_instance = mock_rc.return_value + + u = UserBlocks(domain="domain", token="jwttoken") + + u.unblock("auth0|584ad3c228be27504a2c80d5") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/user-blocks/auth0|584ad3c228be27504a2c80d5" + ) diff --git a/auth0/test/management/test_users.py b/auth0/test/management/test_users.py new file mode 100644 index 00000000..9cab65f6 --- /dev/null +++ b/auth0/test/management/test_users.py @@ -0,0 +1,438 @@ +import unittest +from unittest import mock + +from ...management.users import Users + + +class TestUsers(unittest.TestCase): + def test_init_with_optionals(self): + t = Users(domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.users.RestClient") + def test_list(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.list() + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users", args[0]) + self.assertEqual( + kwargs["params"], + { + "per_page": 25, + "page": 0, + "include_totals": "true", + "sort": None, + "connection": None, + "fields": None, + "include_fields": "true", + "q": None, + "search_engine": None, + }, + ) + + u.list( + page=1, + per_page=50, + sort="s", + connection="con", + q="q", + search_engine="se", + include_totals=False, + fields=["a", "b"], + include_fields=False, + ) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users", args[0]) + self.assertEqual( + kwargs["params"], + { + "per_page": 50, + "page": 1, + "include_totals": "false", + "sort": "s", + "connection": "con", + "fields": "a,b", + "include_fields": "false", + "q": "q", + "search_engine": "se", + }, + ) + + @mock.patch("auth0.management.users.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.create({"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/users", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.users.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get("an-id") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": None, "include_fields": "true"}) + + u.get("an-id", fields=["a", "b"], include_fields=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id", args[0]) + self.assertEqual(kwargs["params"], {"fields": "a,b", "include_fields": "false"}) + + @mock.patch("auth0.management.users.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete("an-id") + + mock_instance.delete.assert_called_with("https://domain/api/v2/users/an-id") + + @mock.patch("auth0.management.users.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.update("an-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.patch.call_args + + self.assertEqual("https://domain/api/v2/users/an-id", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.users.RestClient") + def test_list_organizations(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.list_organizations("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/users/an-id/organizations", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"} + ) + + u.list_organizations(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/organizations", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_list_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.list_roles("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/users/an-id/roles", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"} + ) + + u.list_roles(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/roles", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_remove_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.remove_roles("an-id", ["a", "b"]) + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/roles", args[0]) + self.assertEqual(kwargs["data"], {"roles": ["a", "b"]}) + + @mock.patch("auth0.management.users.RestClient") + def test_add_roles(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.add_roles("an-id", ["a", "b"]) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/roles", args[0]) + self.assertEqual(kwargs["data"], {"roles": ["a", "b"]}) + + @mock.patch("auth0.management.users.RestClient") + def test_list_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.list_permissions("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/users/an-id/permissions", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"} + ) + + u.list_permissions(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/permissions", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_remove_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.remove_permissions("an-id", ["a", "b"]) + + args, kwargs = mock_instance.delete.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/permissions", args[0]) + self.assertEqual(kwargs["data"], {"permissions": ["a", "b"]}) + + @mock.patch("auth0.management.users.RestClient") + def test_add_permissions(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.add_permissions("an-id", ["a", "b"]) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/permissions", args[0]) + self.assertEqual(kwargs["data"], {"permissions": ["a", "b"]}) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_multifactor(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_multifactor("an-id", "provider") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/an-id/multifactor/provider" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_authenticators(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_authenticators("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/an-id/authenticators" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_unlink_user_account(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.unlink_user_account("an-id", "provider", "user-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/an-id/identities/provider/user-id" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_link_user_account(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.link_user_account("user-id", {"a": "b", "c": "d"}) + + args, kwargs = mock_instance.post.call_args + + self.assertEqual("https://domain/api/v2/users/user-id/identities", args[0]) + self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.users.RestClient") + def test_regenerate_recovery_code(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.regenerate_recovery_code("user-id") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/users/user-id/recovery-code-regeneration" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_guardian_enrollments(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_guardian_enrollments("user-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/users/user-id/enrollments" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_log_events(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_log_events("used_id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/users/used_id/logs", args[0]) + self.assertEqual(kwargs["params"]["page"], 0) + self.assertEqual(kwargs["params"]["per_page"], 50) + self.assertIsNone(kwargs["params"]["sort"]) + self.assertEqual(kwargs["params"]["include_totals"], "false") + + @mock.patch("auth0.management.users.RestClient") + def test_invalidate_remembered_browsers(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.invalidate_remembered_browsers("user-id") + + args, kwargs = mock_instance.post.call_args + self.assertEqual( + "https://domain/api/v2/users/user-id/multifactor/actions/invalidate-remember-browser", + args[0], + ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_authentication_methods("user_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_authentication_method_by_id("user_id", "authentication_method_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_create_authentication_method(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.create_authentication_method("user_id", {}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods", data={} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_update_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.update_authentication_methods("user_id", {}) + + mock_instance.put.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods", data={} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_update_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.update_authentication_method_by_id("user_id", "authentication_method_id", {}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id", + data={}, + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_authentication_methods("user_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_authentication_method_by_id("user_id", "authentication_method_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_list_tokensets(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.list_tokensets("an-id") + + args, kwargs = mock_instance.get.call_args + self.assertEqual("https://domain/api/v2/users/an-id/federated-connections-tokensets", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"} + ) + + u.list_tokensets(id="an-id", page=1, per_page=50, include_totals=False) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users/an-id/federated-connections-tokensets", args[0]) + self.assertEqual( + kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_tokenset_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_tokenset_by_id("user_id", "tokenset_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/user_id/federated-connections-tokensets/tokenset_id" + ) \ No newline at end of file diff --git a/auth0/test/management/test_users_by_email.py b/auth0/test/management/test_users_by_email.py new file mode 100644 index 00000000..810c90e1 --- /dev/null +++ b/auth0/test/management/test_users_by_email.py @@ -0,0 +1,41 @@ +import unittest +from unittest import mock + +from ...management.users_by_email import UsersByEmail + + +class TestUsersByEmail(unittest.TestCase): + def test_init_with_optionals(self): + t = UsersByEmail( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.users_by_email.RestClient") + def test_search_users_by_email(self, mock_rc): + mock_instance = mock_rc.return_value + + u = UsersByEmail(domain="domain", token="jwttoken") + u.search_users_by_email("A@B.com") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users-by-email", args[0]) + self.assertEqual( + kwargs["params"], + {"email": "A@B.com", "fields": None, "include_fields": "true"}, + ) + + u.search_users_by_email( + email="a@b.com", fields=["a", "b"], include_fields=False + ) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/users-by-email", args[0]) + self.assertEqual( + kwargs["params"], + {"email": "a@b.com", "fields": "a,b", "include_fields": "false"}, + ) diff --git a/auth0/test_async/__init__.py b/auth0/test_async/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/auth0/test_async/conftest.py b/auth0/test_async/conftest.py new file mode 100644 index 00000000..1247142f --- /dev/null +++ b/auth0/test_async/conftest.py @@ -0,0 +1,7 @@ +import pytest +import random + +@pytest.fixture(autouse=True) +def set_random_seed(): + random.seed(42) + print("Random seeded to 42") \ No newline at end of file diff --git a/auth0/test_async/test_async_auth0.py b/auth0/test_async/test_async_auth0.py new file mode 100644 index 00000000..753666b5 --- /dev/null +++ b/auth0/test_async/test_async_auth0.py @@ -0,0 +1,75 @@ +import re +import unittest +from unittest.mock import ANY, MagicMock + +import pytest +from aioresponses import CallbackResult, aioresponses +from yarl import URL + +from auth0.management.async_auth0 import AsyncAuth0 as Auth0 + +clients = re.compile(r"^https://example\.com/api/v2/clients.*") +factors = re.compile(r"^https://example\.com/api/v2/guardian/factors.*") +payload = {"foo": "bar"} + + +def get_callback(status=200): + mock = MagicMock(return_value=CallbackResult(status=status, payload=payload)) + + def callback(url, **kwargs): + return mock(url, **kwargs) + + return callback, mock + + +class TestAuth0(unittest.IsolatedAsyncioTestCase): + @pytest.mark.asyncio + @aioresponses() + async def test_get(self, mocked): + callback, mock = get_callback() + + mocked.get(clients, callback=callback) + + auth0 = Auth0(domain="example.com", token="jwt") + + self.assertEqual(await auth0.clients.all_async(), payload) + + mock.assert_called_with( + URL("https://example.com/api/v2/clients?include_fields=true"), + allow_redirects=True, + params={"include_fields": "true"}, + headers=ANY, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_shared_session(self, mocked): + callback, mock = get_callback() + callback2, mock2 = get_callback() + + mocked.get(clients, callback=callback) + mocked.put(factors, callback=callback2) + + async with Auth0(domain="example.com", token="jwt") as auth0: + self.assertEqual(await auth0.clients.all_async(), payload) + self.assertEqual( + await auth0.guardian.update_factor_async("factor-1", {"factor": 1}), + payload, + ) + + mock.assert_called_with( + URL("https://example.com/api/v2/clients?include_fields=true"), + allow_redirects=True, + params={"include_fields": "true"}, + headers=ANY, + timeout=ANY, + ) + + mock2.assert_called_with( + URL("https://example.com/api/v2/guardian/factors/factor-1"), + allow_redirects=True, + json={"factor": 1}, + headers=ANY, + timeout=ANY, + ) diff --git a/auth0/test_async/test_async_token_verifier.py b/auth0/test_async/test_async_token_verifier.py new file mode 100644 index 00000000..7559c693 --- /dev/null +++ b/auth0/test_async/test_async_token_verifier.py @@ -0,0 +1,289 @@ +import time +import unittest +from unittest.mock import ANY + +import jwt +import pytest +from aioresponses import aioresponses +from cryptography.hazmat.primitives import serialization +from yarl import URL + +from .. import TokenValidationError +from ..authentication.async_token_verifier import ( + AsyncAsymmetricSignatureVerifier, + AsyncJwksFetcher, + AsyncTokenVerifier, +) +from ..test.authentication.test_token_verifier import ( + JWKS_RESPONSE_MULTIPLE_KEYS, + JWKS_RESPONSE_SINGLE_KEY, + RSA_PUB_KEY_1_JWK, + RSA_PUB_KEY_1_PEM, + RSA_PUB_KEY_2_PEM, +) +from .test_asyncify import get_callback + +JWKS_URI = "https://example.auth0.com/.well-known/jwks.json" + +PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDfytWVSk/4Z6rNu8UZ7C4tnU9x0vj5FCaj4awKZlxVgOR1Kcen +QqDOxJdrXXanTBJbZwh8pk+HpWvqDVgVmKhnt+OkgF//hIXZoJMhDOFVzX504kiZ +cu3bu7kFs+PUfKw5s59tmETFPseA/fIrad9YXHisMkNmPWhuKYJ3WfZAaQIDAQAB +AoGADPSfHL9qlcTanIJsTK3hln5u5PYDt9e0zPP5k7iNS93kW+wJROOUj6PN6EdG +4TSEM4ppcV3naMDo2GnhWY624P6LUB+CbDFzjQKq805vrxJuFnq50blscwVK/ffP +kODBm/gwk+FaliRpQTDAAPWkKbkRfkmPx4JMEmTDBQ45diECQQDxw3qp2+wa5WP5 +9w7AYrDPq4Fd6gIFcmxracROUcdhhMmVHKA9DzTWY46cSoWZoChYhQhhyj8dlP8q +El8aevN9AkEA7PhxcNyff8aehqEQ/Z38bm3P+GgB9EkRinjesba2CqhEI5okzvb7 +OIYdszgQUBqGKlST0a7s9KuTpd7moyy8XQJAY8hjk0HCxCMTTXMLspnJEh1eKo3P +wcHFP9wKeqzEFtrAfHuxIyJok2fJz3XuiEaTAF3/5KSdwi7h1dJ5UCuY3QJAM9rF +0CGnEWngJKu4MRdSNsP232+7Bb67hOagLJlDyp85keTYKyXmoV7PvvkEsNKtCzRI +yHiTx5KIE6LsK0bNzQJBAMV+1KyI8ua1XmqLDaOexvBPM86HnuP+8u5CthgrXyGm +nh9gurwbs/lBRYV/d4XBLj+dzHb2zC0Jo7u96wrOObw= +-----END RSA PRIVATE KEY-----""" + +PUBLIC_KEY = { + "kty": "RSA", + "e": "AQAB", + "kid": "kid-1", + "n": "38rVlUpP-GeqzbvFGewuLZ1PcdL4-RQmo-GsCmZcVYDkdSnHp0KgzsSXa112p0wSW2cIfKZPh6Vr6g1YFZioZ7fjpIBf_4SF2aCTIQzhVc1-dOJImXLt27u5BbPj1HysObOfbZhExT7HgP3yK2nfWFx4rDJDZj1obimCd1n2QGk", +} + + +def get_pem_bytes(rsa_public_key): + return rsa_public_key.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + + +class TestAsyncAsymmetricSignatureVerifier(unittest.IsolatedAsyncioTestCase): + @pytest.mark.asyncio + @aioresponses() + async def test_async_asymmetric_verifier_fetches_key(self, mocked): + callback, mock = get_callback(200, JWKS_RESPONSE_SINGLE_KEY) + mocked.get(JWKS_URI, callback=callback) + + verifier = AsyncAsymmetricSignatureVerifier(JWKS_URI) + + key = await verifier._fetch_key("test-key-1") + + self.assertEqual(get_pem_bytes(key), RSA_PUB_KEY_1_PEM) + + +class TestAsyncJwksFetcher(unittest.IsolatedAsyncioTestCase): + @pytest.mark.asyncio + @aioresponses() + @unittest.mock.patch( + "auth0.authentication.token_verifier.time.time", return_value=0 + ) + async def test_async_get_jwks_json_twice_on_cache_expired( + self, mocked, mocked_time + ): + fetcher = AsyncJwksFetcher(JWKS_URI, cache_ttl=100) + + callback, mock = get_callback(200, JWKS_RESPONSE_SINGLE_KEY) + mocked.get(JWKS_URI, callback=callback) + mocked.get(JWKS_URI, callback=callback) + + key_1 = await fetcher.get_key("test-key-1") + expected_key_1_pem = get_pem_bytes(key_1) + self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual(mock.call_count, 1) + + mocked_time.return_value = 200 + + # 2 seconds has passed, cache should be expired + key_1 = await fetcher.get_key("test-key-1") + expected_key_1_pem = get_pem_bytes(key_1) + self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual(mock.call_count, 2) + + @pytest.mark.asyncio + @aioresponses() + async def test_async_get_jwks_json_once_on_cache_hit(self, mocked): + fetcher = AsyncJwksFetcher(JWKS_URI, cache_ttl=1) + + callback, mock = get_callback(200, JWKS_RESPONSE_MULTIPLE_KEYS) + mocked.get(JWKS_URI, callback=callback) + mocked.get(JWKS_URI, callback=callback) + + key_1 = await fetcher.get_key("test-key-1") + key_2 = await fetcher.get_key("test-key-2") + expected_key_1_pem = get_pem_bytes(key_1) + expected_key_2_pem = get_pem_bytes(key_2) + self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) + self.assertEqual(expected_key_2_pem, RSA_PUB_KEY_2_PEM) + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual(mock.call_count, 1) + + @pytest.mark.asyncio + @aioresponses() + async def test_async_fetches_jwks_json_forced_on_cache_miss(self, mocked): + fetcher = AsyncJwksFetcher(JWKS_URI, cache_ttl=1) + + callback, mock = get_callback(200, {"keys": [RSA_PUB_KEY_1_JWK]}) + mocked.get(JWKS_URI, callback=callback) + + # Triggers the first call + key_1 = await fetcher.get_key("test-key-1") + expected_key_1_pem = get_pem_bytes(key_1) + self.assertEqual(expected_key_1_pem, RSA_PUB_KEY_1_PEM) + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual(mock.call_count, 1) + + callback, mock = get_callback(200, JWKS_RESPONSE_MULTIPLE_KEYS) + mocked.get(JWKS_URI, callback=callback) + + # Triggers the second call + key_2 = await fetcher.get_key("test-key-2") + expected_key_2_pem = get_pem_bytes(key_2) + self.assertEqual(expected_key_2_pem, RSA_PUB_KEY_2_PEM) + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual(mock.call_count, 1) + + @pytest.mark.asyncio + @aioresponses() + async def test_async_fetches_jwks_json_once_on_cache_miss(self, mocked): + fetcher = AsyncJwksFetcher(JWKS_URI, cache_ttl=1) + + callback, mock = get_callback(200, JWKS_RESPONSE_SINGLE_KEY) + mocked.get(JWKS_URI, callback=callback) + + with self.assertRaises(Exception) as err: + await fetcher.get_key("missing-key") + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual( + str(err.exception), 'RSA Public Key with ID "missing-key" was not found.' + ) + self.assertEqual(mock.call_count, 1) + + @pytest.mark.asyncio + @aioresponses() + async def test_async_fails_to_fetch_jwks_json_after_retrying_twice(self, mocked): + fetcher = AsyncJwksFetcher(JWKS_URI, cache_ttl=1) + + callback, mock = get_callback(500, {}) + mocked.get(JWKS_URI, callback=callback) + mocked.get(JWKS_URI, callback=callback) + + with self.assertRaises(Exception) as err: + await fetcher.get_key("id1") + + mock.assert_called_with( + URL("https://example.auth0.com/.well-known/jwks.json"), + allow_redirects=True, + params=None, + headers=ANY, + timeout=ANY, + ) + self.assertEqual( + str(err.exception), 'RSA Public Key with ID "id1" was not found.' + ) + self.assertEqual(mock.call_count, 2) + + +class TestAsyncTokenVerifier(unittest.IsolatedAsyncioTestCase): + @pytest.mark.asyncio + @aioresponses() + async def test_RS256_token_signature_passes(self, mocked): + callback, mock = get_callback(200, {"keys": [PUBLIC_KEY]}) + mocked.get(JWKS_URI, callback=callback) + + issuer = "https://tokens-test.auth0.com/" + audience = "tokens-test-123" + token = jwt.encode( + { + "iss": issuer, + "sub": "auth0|123456789", + "aud": audience, + "exp": int(time.time()) + 86400, + "iat": int(time.time()), + }, + PRIVATE_KEY, + algorithm="RS256", + headers={"kid": "kid-1"}, + ) + + tv = AsyncTokenVerifier( + signature_verifier=AsyncAsymmetricSignatureVerifier(JWKS_URI), + issuer=issuer, + audience=audience, + ) + payload = await tv.verify(token) + self.assertEqual(payload["sub"], "auth0|123456789") + + @pytest.mark.asyncio + @aioresponses() + async def test_RS256_token_signature_fails(self, mocked): + callback, mock = get_callback( + 200, {"keys": [RSA_PUB_KEY_1_JWK]} + ) # different pub key + mocked.get(JWKS_URI, callback=callback) + + issuer = "https://tokens-test.auth0.com/" + audience = "tokens-test-123" + token = jwt.encode( + { + "iss": issuer, + "sub": "auth0|123456789", + "aud": audience, + "exp": int(time.time()) + 86400, + "iat": int(time.time()), + }, + PRIVATE_KEY, + algorithm="RS256", + headers={"kid": "test-key-1"}, + ) + + tv = AsyncTokenVerifier( + signature_verifier=AsyncAsymmetricSignatureVerifier(JWKS_URI), + issuer=issuer, + audience=audience, + ) + + with self.assertRaises(TokenValidationError) as err: + await tv.verify(token) + self.assertEqual(str(err.exception), "Invalid token signature.") diff --git a/auth0/test_async/test_asyncify.py b/auth0/test_async/test_asyncify.py new file mode 100644 index 00000000..acc3f54d --- /dev/null +++ b/auth0/test_async/test_asyncify.py @@ -0,0 +1,263 @@ +import base64 +import json +import platform +import re +import sys +import unittest +from tempfile import TemporaryFile +from unittest.mock import ANY, MagicMock + +import aiohttp +import pytest +from aioresponses import CallbackResult, aioresponses +from yarl import URL + +from auth0.asyncify import asyncify +from auth0.authentication import GetToken, Users +from auth0.management import Clients, Guardian, Jobs + +clients = re.compile(r"^https://example\.com/api/v2/clients.*") +token = re.compile(r"^https://example\.com/oauth/token.*") +user_info = re.compile(r"^https://example\.com/userinfo.*") +factors = re.compile(r"^https://example\.com/api/v2/guardian/factors.*") +users_imports = re.compile(r"^https://example\.com/api/v2/jobs/users-imports.*") +payload = {"foo": "bar"} + +telemetry = base64.b64encode( + json.dumps( + { + "name": "auth0-python", + "version": sys.modules["auth0"].__version__, + "env": { + "python": platform.python_version(), + }, + } + ).encode("utf-8") +).decode() + +headers = { + "User-Agent": f"Python/{platform.python_version()}", + "Authorization": "Bearer jwt", + "Content-Type": "application/json", + "Auth0-Client": telemetry, +} + + +def get_callback(status=200, response=None): + mock = MagicMock( + return_value=CallbackResult(status=status, payload=response or payload) + ) + + def callback(url, **kwargs): + return mock(url, **kwargs) + + return callback, mock + + +class TestAsyncify(unittest.IsolatedAsyncioTestCase): + @pytest.mark.asyncio + @aioresponses() + async def test_get(self, mocked): + callback, mock = get_callback() + mocked.get(clients, callback=callback) + c = asyncify(Clients)(domain="example.com", token="jwt") + self.assertEqual(await c.all_async(), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/clients?include_fields=true"), + allow_redirects=True, + params={"include_fields": "true"}, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_post(self, mocked): + callback, mock = get_callback() + mocked.post(clients, callback=callback) + c = asyncify(Clients)(domain="example.com", token="jwt") + data = {"client": 1} + self.assertEqual(await c.create_async(data), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/clients"), + allow_redirects=True, + json=data, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_post_auth(self, mocked): + callback, mock = get_callback() + mocked.post(token, callback=callback) + c = asyncify(GetToken)("example.com", "cid", client_secret="clsec") + self.assertEqual( + await c.login_async(username="usrnm", password="pswd"), payload + ) + mock.assert_called_with( + URL("https://example.com/oauth/token"), + allow_redirects=True, + json={ + "client_id": "cid", + "username": "usrnm", + "password": "pswd", + "realm": None, + "scope": None, + "audience": None, + "grant_type": "http://auth0.com/oauth/grant-type/password-realm", + "client_secret": "clsec", + }, + headers={i: headers[i] for i in headers if i != "Authorization"}, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_user_info(self, mocked): + callback, mock = get_callback() + mocked.get(user_info, callback=callback) + c = asyncify(Users)(domain="example.com") + self.assertEqual( + await c.userinfo_async(access_token="access-token-example"), payload + ) + mock.assert_called_with( + URL("https://example.com/userinfo"), + headers={**headers, "Authorization": "Bearer access-token-example"}, + timeout=ANY, + allow_redirects=True, + params=None, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_file_post(self, mocked): + callback, mock = get_callback() + mocked.post(users_imports, callback=callback) + j = asyncify(Jobs)(domain="example.com", token="jwt") + users = TemporaryFile() + self.assertEqual(await j.import_users_async("connection-1", users), payload) + file_port_headers = headers.copy() + file_port_headers.pop("Content-Type") + mock.assert_called_with( + URL("https://example.com/api/v2/jobs/users-imports"), + allow_redirects=True, + data={ + "connection_id": "connection-1", + "upsert": "false", + "send_completion_email": "true", + "external_id": None, + "users": users, + }, + headers=file_port_headers, + timeout=ANY, + ) + users.close() + + @pytest.mark.asyncio + @aioresponses() + async def test_patch(self, mocked): + callback, mock = get_callback() + mocked.patch(clients, callback=callback) + c = asyncify(Clients)(domain="example.com", token="jwt") + data = {"client": 1} + self.assertEqual(await c.update_async("client-1", data), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/clients/client-1"), + allow_redirects=True, + json=data, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_put(self, mocked): + callback, mock = get_callback() + mocked.put(factors, callback=callback) + g = asyncify(Guardian)(domain="example.com", token="jwt") + data = {"factor": 1} + self.assertEqual(await g.update_factor_async("factor-1", data), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/guardian/factors/factor-1"), + allow_redirects=True, + json=data, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_delete(self, mocked): + callback, mock = get_callback() + mocked.delete(clients, callback=callback) + c = asyncify(Clients)(domain="example.com", token="jwt") + self.assertEqual(await c.delete_async("client-1"), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/clients/client-1"), + allow_redirects=True, + params={}, + json=None, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_shared_session(self, mocked): + callback, mock = get_callback() + mocked.get(clients, callback=callback) + async with asyncify(Clients)(domain="example.com", token="jwt") as c: + self.assertEqual(await c.all_async(), payload) + mock.assert_called_with( + URL("https://example.com/api/v2/clients?include_fields=true"), + allow_redirects=True, + params={"include_fields": "true"}, + headers=headers, + timeout=ANY, + ) + + @pytest.mark.asyncio + @aioresponses() + async def test_rate_limit(self, mocked): + callback, mock = get_callback(status=429) + mocked.get(clients, callback=callback) + mocked.get(clients, callback=callback) + mocked.get(clients, callback=callback) + mocked.get(clients, payload=payload) + c = asyncify(Clients)(domain="example.com", token="jwt") + rest_client = c._async_client.client + rest_client._skip_sleep = True + self.assertEqual(await c.all_async(), payload) + self.assertEqual(3, mock.call_count) + (a, b, c) = rest_client._metrics["backoff"] + self.assertTrue(100 <= a < b < c <= 1000) + + @pytest.mark.asyncio + @aioresponses() + async def test_rate_limit_post(self, mocked): + callback, mock = get_callback(status=429) + mocked.post(clients, callback=callback) + mocked.post(clients, callback=callback) + mocked.post(clients, callback=callback) + mocked.post(clients, payload=payload) + c = asyncify(Clients)(domain="example.com", token="jwt") + rest_client = c._async_client.client + rest_client._skip_sleep = True + self.assertEqual(await c.create_async({}), payload) + self.assertEqual(3, mock.call_count) + + @pytest.mark.asyncio + @aioresponses() + async def test_timeout(self, mocked): + callback, mock = get_callback() + mocked.get(clients, callback=callback) + c = asyncify(Clients)(domain="example.com", token="jwt", timeout=(8.8, 9.9)) + self.assertEqual(await c.all_async(), payload) + mock.assert_called_with( + ANY, + allow_redirects=ANY, + params=ANY, + headers=ANY, + timeout=aiohttp.ClientTimeout(sock_connect=8.8, sock_read=9.9), + ) diff --git a/auth0/types.py b/auth0/types.py new file mode 100644 index 00000000..c1929cf2 --- /dev/null +++ b/auth0/types.py @@ -0,0 +1,5 @@ +from typing import Any, Dict, List, Tuple, Union + +TimeoutType = Union[float, Tuple[float, float]] + +RequestData = Union[Dict[str, Any], List[Any]] diff --git a/auth0/utils.py b/auth0/utils.py new file mode 100644 index 00000000..807e9016 --- /dev/null +++ b/auth0/utils.py @@ -0,0 +1,11 @@ +def is_async_available() -> bool: + try: + import asyncio + + import aiohttp + + return True + except ImportError: # pragma: no cover + pass + + return False diff --git a/auth0/v3/__init__.py b/auth0/v3/__init__.py deleted file mode 100644 index 79dde017..00000000 --- a/auth0/v3/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .exceptions import Auth0Error, RateLimitError, TokenValidationError diff --git a/auth0/v3/authentication/authorize_client.py b/auth0/v3/authentication/authorize_client.py deleted file mode 100644 index 44ecb344..00000000 --- a/auth0/v3/authentication/authorize_client.py +++ /dev/null @@ -1,31 +0,0 @@ -from .base import AuthenticationBase - - -class AuthorizeClient(AuthenticationBase): - - """Authorize Client - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def authorize(self, client_id, audience=None, state=None, redirect_uri=None, - response_type='code', scope='openid', organization=None, invitation=None): - """Authorization code grant - - This is the OAuth 2.0 grant that regular web apps utilize in order to access an API. - """ - params = { - 'client_id': client_id, - 'audience': audience, - 'response_type': response_type, - 'scope': scope, - 'state': state, - 'redirect_uri': redirect_uri, - 'organization': organization, - 'invitation': invitation - } - - return self.get( - '{}://{}/authorize'.format(self.protocol, self.domain), - params=params) diff --git a/auth0/v3/authentication/base.py b/auth0/v3/authentication/base.py deleted file mode 100644 index 4d0ff93e..00000000 --- a/auth0/v3/authentication/base.py +++ /dev/null @@ -1,137 +0,0 @@ -import base64 -import json -import sys -import platform -import requests -from ..exceptions import Auth0Error, RateLimitError - -UNKNOWN_ERROR = 'a0.sdk.internal.unknown' - - -class AuthenticationBase(object): - """Base authentication object providing simple REST methods. - - Args: - telemetry (bool, optional): Enable or disable Telemetry - (defaults to True) - timeout (float or tuple, optional): Change the requests - connect and read timeout. Pass a tuple to specify - both values separately or a float to set both to it. - (defaults to 5.0 for both) - """ - - def __init__(self, domain, telemetry=True, timeout=5.0, protocol="https"): - self.domain = domain - self.timeout = timeout - self.protocol = protocol - self.base_headers = {'Content-Type': 'application/json'} - - if telemetry: - py_version = platform.python_version() - version = sys.modules['auth0'].__version__ - - auth0_client = json.dumps({ - 'name': 'auth0-python', - 'version': version, - 'env': { - 'python': py_version, - } - }).encode('utf-8') - - self.base_headers.update({ - 'User-Agent': 'Python/{}'.format(py_version), - 'Auth0-Client': base64.b64encode(auth0_client), - }) - - def post(self, url, data=None, headers=None): - request_headers = self.base_headers.copy() - request_headers.update(headers or {}) - response = requests.post(url=url, json=data, headers=request_headers, timeout=self.timeout) - return self._process_response(response) - - def get(self, url, params=None, headers=None): - request_headers = self.base_headers.copy() - request_headers.update(headers or {}) - response = requests.get(url=url, params=params, headers=request_headers, timeout=self.timeout) - return self._process_response(response) - - def _process_response(self, response): - return self._parse(response).content() - - def _parse(self, response): - if not response.text: - return EmptyResponse(response.status_code) - try: - return JsonResponse(response) - except ValueError: - return PlainResponse(response) - - -class Response(object): - def __init__(self, status_code, content, headers): - self._status_code = status_code - self._content = content - self._headers = headers - - def content(self): - if not self._is_error(): - return self._content - - if self._status_code == 429: - reset_at = int(self._headers.get('x-ratelimit-reset', '-1')) - raise RateLimitError(error_code=self._error_code(), - message=self._error_message(), - reset_at=reset_at) - - raise Auth0Error(status_code=self._status_code, - error_code=self._error_code(), - message=self._error_message()) - - def _is_error(self): - return self._status_code is None or self._status_code >= 400 - - # Adding these methods to force implementation in subclasses because they are references in this parent class - def _error_code(self): - raise NotImplementedError - - def _error_message(self): - raise NotImplementedError - - -class JsonResponse(Response): - def __init__(self, response): - content = json.loads(response.text) - super(JsonResponse, self).__init__(response.status_code, content, response.headers) - - def _error_code(self): - if 'error' in self._content: - return self._content.get('error') - elif 'code' in self._content: - return self._content.get('code') - else: - return UNKNOWN_ERROR - - def _error_message(self): - return self._content.get('error_description', '') - - -class PlainResponse(Response): - def __init__(self, response): - super(PlainResponse, self).__init__(response.status_code, response.text, response.headers) - - def _error_code(self): - return UNKNOWN_ERROR - - def _error_message(self): - return self._content - - -class EmptyResponse(Response): - def __init__(self, status_code): - super(EmptyResponse, self).__init__(status_code, '', {}) - - def _error_code(self): - return UNKNOWN_ERROR - - def _error_message(self): - return '' diff --git a/auth0/v3/authentication/database.py b/auth0/v3/authentication/database.py deleted file mode 100644 index e19f87be..00000000 --- a/auth0/v3/authentication/database.py +++ /dev/null @@ -1,108 +0,0 @@ -import warnings - -from .base import AuthenticationBase - - -class Database(AuthenticationBase): - """Database & Active Directory / LDAP Authentication. - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def login(self, client_id, username, password, connection, id_token=None, - grant_type='password', device=None, scope='openid'): - """Login using username and password - - Given the user credentials and the connection specified, it will do - the authentication on the provider and return a dict with the - access_token and id_token. This endpoint only works for database - connections, passwordless connections, Active Directory/LDAP, - Windows Azure AD and ADFS. - """ - warnings.warn("/oauth/ro will be deprecated in future releases", DeprecationWarning) - - body = { - 'client_id': client_id, - 'username': username, - 'password': password, - 'connection': connection, - 'grant_type': grant_type, - 'scope': scope, - } - if id_token: - body.update({'id_token': id_token}) - if device: - body.update({'device': device}) - return self.post('{}://{}/oauth/ro'.format(self.protocol, self.domain), data=body) - - def signup(self, client_id, email, password, connection, username=None, user_metadata=None, - given_name=None, family_name=None, name=None, nickname=None, picture=None): - """Signup using email and password. - - Args: - client_id (str): ID of the application to use. - - email (str): The user's email address. - - password (str): The user's desired password. - - connection (str): The name of the database connection where this user should be created. - - username (str, optional): The user's username, if required by the database connection. - - user_metadata (dict, optional): Additional key-value information to store for the user. - Some limitations apply, see: https://auth0.com/docs/metadata#metadata-restrictions - - given_name (str, optional): The user's given name(s). - - family_name (str, optional): The user's family name(s). - - name (str, optional): The user's full name. - - nickname (str, optional): The user's nickname. - - picture (str, optional): A URI pointing to the user's picture. - - - See: https://auth0.com/docs/api/authentication#signup - """ - body = { - 'client_id': client_id, - 'email': email, - 'password': password, - 'connection': connection, - } - if username: - body.update({'username': username}) - if user_metadata: - body.update({'user_metadata': user_metadata}) - if given_name: - body.update({'given_name': given_name}) - if family_name: - body.update({'family_name': family_name}) - if name: - body.update({'name': name}) - if nickname: - body.update({'nickname': nickname}) - if picture: - body.update({'picture': picture}) - - return self.post('{}://{}/dbconnections/signup'.format(self.protocol, self.domain), data=body) - - def change_password(self, client_id, email, connection, password=None): - """Asks to change a password for a given user. - - client_id (str): ID of the application to use. - - email (str): The user's email address. - - connection (str): The name of the database connection where this user should be created. - """ - body = { - 'client_id': client_id, - 'email': email, - 'connection': connection, - } - - return self.post('{}://{}/dbconnections/change_password'.format(self.protocol, self.domain), data=body) diff --git a/auth0/v3/authentication/delegated.py b/auth0/v3/authentication/delegated.py deleted file mode 100644 index 9df0233f..00000000 --- a/auth0/v3/authentication/delegated.py +++ /dev/null @@ -1,37 +0,0 @@ -from .base import AuthenticationBase - - -class Delegated(AuthenticationBase): - """Delegated authentication endpoints. - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def get_token(self, client_id, target, api_type, grant_type, - id_token=None, refresh_token=None, scope='openid'): - - """Obtain a delegation token. - """ - - if id_token and refresh_token: - raise ValueError('Only one of id_token or refresh_token ' - 'can be None') - - data = { - 'client_id': client_id, - 'grant_type': grant_type, - 'target': target, - 'scope': scope, - 'api_type': api_type, - } - - if id_token: - data.update({'id_token': id_token}) - elif refresh_token: - data.update({'refresh_token': refresh_token}) - else: - raise ValueError('Either id_token or refresh_token must ' - 'have a value') - - return self.post('{}://{}/delegation'.format(self.protocol, self.domain), data=data) diff --git a/auth0/v3/authentication/enterprise.py b/auth0/v3/authentication/enterprise.py deleted file mode 100644 index e8ff9084..00000000 --- a/auth0/v3/authentication/enterprise.py +++ /dev/null @@ -1,28 +0,0 @@ -from .base import AuthenticationBase - - -class Enterprise(AuthenticationBase): - - """Enterprise endpoints. - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def saml_metadata(self, client_id): - """Get SAML2.0 Metadata. - - Args: - client_id (str): Client Id of the application to get the SAML metadata for. - """ - - return self.get(url='{}://{}/samlp/metadata/{}'.format(self.protocol, self.domain, client_id)) - - def wsfed_metadata(self): - """Returns the WS-Federation Metadata. - """ - - url = '{}://{}/wsfed/FederationMetadata' \ - '/2007-06/FederationMetadata.xml' - - return self.get(url=url.format(self.protocol, self.domain)) diff --git a/auth0/v3/authentication/get_token.py b/auth0/v3/authentication/get_token.py deleted file mode 100644 index bb02ad42..00000000 --- a/auth0/v3/authentication/get_token.py +++ /dev/null @@ -1,239 +0,0 @@ -from .base import AuthenticationBase - - -class GetToken(AuthenticationBase): - - """/oauth/token related endpoints - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def authorization_code(self, client_id, client_secret, code, - redirect_uri, grant_type='authorization_code'): - """Authorization code grant - - This is the OAuth 2.0 grant that regular web apps utilize in order - to access an API. Use this endpoint to exchange an Authorization Code - for a Token. - - Args: - grant_type (str): Denotes the flow you're using. For authorization code - use authorization_code - - client_id (str): your application's client Id - - client_secret (str): your application's client Secret - - code (str): The Authorization Code received from the /authorize Calls - - redirect_uri (str, optional): This is required only if it was set at - the GET /authorize endpoint. The values must match - - Returns: - access_token, id_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'client_secret': client_secret, - 'code': code, - 'grant_type': grant_type, - 'redirect_uri': redirect_uri, - } - ) - - def authorization_code_pkce(self, client_id, code_verifier, code, - redirect_uri, grant_type='authorization_code'): - """Authorization code pkce grant - - This is the OAuth 2.0 grant that mobile apps utilize in order to access an API. - Use this endpoint to exchange an Authorization Code for a Token. - - Args: - grant_type (str): Denotes the flow you're using. For authorization code pkce - use authorization_code - - client_id (str): your application's client Id - - code_verifier (str): Cryptographically random key that was used to generate - the code_challenge passed to /authorize. - - code (str): The Authorization Code received from the /authorize Calls - - redirect_uri (str, optional): This is required only if it was set at - the GET /authorize endpoint. The values must match - - Returns: - access_token, id_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'code_verifier': code_verifier, - 'code': code, - 'grant_type': grant_type, - 'redirect_uri': redirect_uri, - } - ) - - def client_credentials(self, client_id, client_secret, audience, - grant_type='client_credentials'): - """Client credentials grant - - This is the OAuth 2.0 grant that server processes utilize in - order to access an API. Use this endpoint to directly request - an access_token by using the Application Credentials (a Client Id and - a Client Secret). - - Args: - grant_type (str): Denotes the flow you're using. For client credentials - use client_credentials - - client_id (str): your application's client Id - - client_secret (str): your application's client Secret - - audience (str): The unique identifier of the target API you want to access. - - Returns: - access_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'client_secret': client_secret, - 'audience': audience, - 'grant_type': grant_type, - } - ) - - def login(self, client_id, client_secret, username, password, scope, realm, - audience, grant_type='http://auth0.com/oauth/grant-type/password-realm'): - """Calls /oauth/token endpoint with password-realm grant type - - - This is the OAuth 2.0 grant that highly trusted apps utilize in order - to access an API. In this flow the end-user is asked to fill in credentials - (username/password) typically using an interactive form in the user-agent - (browser). This information is later on sent to the client and Auth0. - It is therefore imperative that the client is absolutely trusted with - this information. - - Args: - grant_type (str): Denotes the flow you're using. For password realm - use http://auth0.com/oauth/grant-type/password-realm - - client_id (str): your application's client Id - - client_secret (str): your application's client Secret - - audience (str): The unique identifier of the target API you want to access. - - username (str): Resource owner's identifier - - password (str): resource owner's Secret - - scope(str): String value of the different scopes the client is asking for. - Multiple scopes are separated with whitespace. - - realm (str): String value of the realm the user belongs. - Set this if you want to add realm support at this grant. - - Returns: - access_token, id_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'username': username, - 'password': password, - 'realm': realm, - 'client_secret': client_secret, - 'scope': scope, - 'audience': audience, - 'grant_type': grant_type - } - ) - - def refresh_token(self, client_id, client_secret, refresh_token, grant_type='refresh_token', scope=''): - """Calls /oauth/token endpoint with refresh token grant type - - Use this endpoint to refresh an access token, using the refresh token you got during authorization. - - Args: - grant_type (str): Denotes the flow you're using. For refresh token - use refresh_token - - client_id (str): your application's client Id - - client_secret (str): your application's client Secret - - refresh_token (str): The refresh token returned from the initial token request. - - scope (str): String value of the different scopes the client is asking for. - Multiple scopes are separated with whitespace. - - Returns: - access_token, id_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'client_secret': client_secret, - 'refresh_token': refresh_token, - 'scope': scope, - 'grant_type': grant_type - } - ) - - def passwordless_login(self, client_id, client_secret, username, otp, realm, scope, audience): - """Calls /oauth/token endpoint with http://auth0.com/oauth/grant-type/passwordless/otp grant type - - Once the verification code was received, login the user using this endpoint with their - phone number/email and verification code. - - Args: - client_id (str): your application's client Id - - client_secret (str): your application's client Secret. Only required for Regular Web Apps. - - username (str): The user's phone number or email address. - - otp (str): the user's verification code. - - realm (str): use 'sms' or 'email'. - Should be the same as the one used to start the passwordless flow. - - scope(str): String value of the different scopes the client is asking for. - Multiple scopes are separated with whitespace. - - audience (str): The unique identifier of the target API you want to access. - - Returns: - access_token, id_token - """ - - return self.post( - '{}://{}/oauth/token'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'username': username, - 'otp': otp, - 'realm': realm, - 'client_secret': client_secret, - 'scope': scope, - 'audience': audience, - 'grant_type': 'http://auth0.com/oauth/grant-type/passwordless/otp' - } - ) \ No newline at end of file diff --git a/auth0/v3/authentication/logout.py b/auth0/v3/authentication/logout.py deleted file mode 100644 index fad72457..00000000 --- a/auth0/v3/authentication/logout.py +++ /dev/null @@ -1,42 +0,0 @@ -from .base import AuthenticationBase -try: - from urllib.parse import quote_plus -except ImportError: - from urllib import quote_plus - - -class Logout(AuthenticationBase): - - """Logout Endpoint - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def logout(self, client_id, return_to, federated=False): - """Logout - - Use this endpoint to logout a user. If you want to navigate the user to a - specific URL after the logout, set that URL at the returnTo parameter. - The URL should be included in any the appropriate Allowed Logout URLs list: - - Args: - client_id (str): The client_id of your application. - - returnTo (str): URL to redirect the user after the logout. - - federated (bool): Querystring parameter to log the user out of the IdP - """ - return_to = quote_plus(return_to) - - if federated is True: - return self.get( - '{}://{}/v2/logout?federated&client_id={}&returnTo={}'.format( - self.protocol, self.domain, client_id, return_to) - ) - return self.get( - '{}://{}/v2/logout?client_id={}&returnTo={}'.format(self.protocol, - self.domain, - client_id, - return_to) - ) diff --git a/auth0/v3/authentication/passwordless.py b/auth0/v3/authentication/passwordless.py deleted file mode 100644 index 7ba3ff3f..00000000 --- a/auth0/v3/authentication/passwordless.py +++ /dev/null @@ -1,111 +0,0 @@ -import warnings -from .base import AuthenticationBase - - -class Passwordless(AuthenticationBase): - - """Passwordless connections endpoints. - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def email(self, client_id, email, send='link', auth_params=None, client_secret=None): - """Start flow sending an email. - - Given the user email address, it will send an email with: - - - A link (default, send:"link"). You can then authenticate with - this user opening the link and he will be automatically logged in - to the application. Optionally, you can append/override - parameters to the link (like scope, redirect_uri, protocol, - response_type, etc.) using auth_params dict. - - - A verification code (send:"code"). You can then authenticate with - this user using email as username and code as password. - - Complete the authentication using the get_token.passwordless_login method. - - Args: - client_id (str): Client Id of the application. - - email (str): Email address. - - send (str, optional): Can be: 'link' or 'code'. Defaults to 'link'. - - auth_params (dict, optional): Parameters to append or override. - - client_secret (str): Client Secret of the application. - """ - - data={ - 'client_id': client_id, - 'connection': 'email', - 'email': email, - 'send': send, - } - if auth_params: - data.update({'authParams': auth_params}) - if client_secret: - data.update({'client_secret': client_secret}) - - return self.post( - '{}://{}/passwordless/start'.format(self.protocol, self.domain), - data=data - ) - - def sms(self, client_id, phone_number, client_secret=None): - """Start flow sending an SMS message. - - Given the user phone number, it will send an SMS with - a verification code. You can then authenticate with - this user using phone number as username and code as password. - - Complete the authentication using the get_token.passwordless_login method. - - Args: - client_id (str): Client Id of the application. - - client_secret (str): Client Secret of the application. - - phone_number (str): Phone number. - """ - - data={ - 'client_id': client_id, - 'connection': 'sms', - 'phone_number': phone_number, - } - if client_secret: - data.update({'client_secret': client_secret}) - - return self.post( - '{}://{}/passwordless/start'.format(self.protocol, self.domain), - data=data - ) - - def sms_login(self, client_id, phone_number, code, scope='openid'): - """Login using phone number/verification code. - - Args: - client_id (str): Client Id of the application. - - phone_number (str): Phone number. - - code (str): Code received in the SMS. - - scope (str, optional): Scope to use. Defaults to 'openid'. - """ - warnings.warn("/oauth/ro will be deprecated in future releases", DeprecationWarning) - - return self.post( - '{}://{}/oauth/ro'.format(self.protocol, self.domain), - data={ - 'client_id': client_id, - 'connection': 'sms', - 'grant_type': 'password', - 'username': phone_number, - 'password': code, - 'scope': scope, - } - ) diff --git a/auth0/v3/authentication/revoke_token.py b/auth0/v3/authentication/revoke_token.py deleted file mode 100644 index 5d746fa0..00000000 --- a/auth0/v3/authentication/revoke_token.py +++ /dev/null @@ -1,37 +0,0 @@ -from .base import AuthenticationBase - - -class RevokeToken(AuthenticationBase): - """Revoke Refresh Token endpoint - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def revoke_refresh_token(self, client_id, token, client_secret=None): - """Revokes a Refresh Token if it has been compromised - - Each revocation request invalidates not only the specific token, but all other tokens - based on the same authorization grant. This means that all Refresh Tokens that have - been issued for the same user, application, and audience will be revoked. - - Args: - client_id (str): The Client ID for your Application - - token (str): The Refresh Token you want to revoke - - client_secret (str, optional): The Client Secret for your Application. - Required for confidential applications. - See: https://auth0.com/docs/applications/application-types#confidential-applications - - See: https://auth0.com/docs/api/authentication#refresh-token - """ - body = { - 'client_id': client_id, - 'token': token, - } - - if client_secret: - body.update({'client_secret': client_secret}) - - return self.post('{}://{}/oauth/revoke'.format(self.protocol, self.domain), data=body) diff --git a/auth0/v3/authentication/token_verifier.py b/auth0/v3/authentication/token_verifier.py deleted file mode 100644 index 97e3c466..00000000 --- a/auth0/v3/authentication/token_verifier.py +++ /dev/null @@ -1,349 +0,0 @@ -"""Token Verifier module""" -import json -import time - -import jwt -import requests - -from auth0.v3.exceptions import TokenValidationError - - -class SignatureVerifier(object): - """Abstract class that will verify a given JSON web token's signature - using the key fetched internally given its key id. - - Args: - algorithm (str): The expected signing algorithm (e.g. RS256). - """ - - DISABLE_JWT_CHECKS = { - "verify_signature": True, - "verify_exp": False, - "verify_nbf": False, - "verify_iat": False, - "verify_aud": False, - "verify_iss": False, - "require_exp": False, - "require_iat": False, - "require_nbf": False, - } - - def __init__(self, algorithm): - if not algorithm or type(algorithm) != str: - raise ValueError("algorithm must be specified.") - self._algorithm = algorithm - - def _fetch_key(self, key_id=None): - """Obtains the key associated to the given key id. - Must be implemented by subclasses. - - Args: - key_id (str, optional): The id of the key to fetch. - - Returns: - the key to use for verifying a cryptographic signature - """ - raise NotImplementedError - - def verify_signature(self, token): - """Verifies the signature of the given JSON web token. - - Args: - token (str): The JWT to get its signature verified. - - Raises: - TokenValidationError: if the token cannot be decoded, the algorithm is invalid - or the token's signature doesn't match the calculated one. - """ - try: - header = jwt.get_unverified_header(token) - except jwt.exceptions.DecodeError: - raise TokenValidationError("token could not be decoded.") - - alg = header.get('alg', None) - if alg != self._algorithm: - raise TokenValidationError( - 'Signature algorithm of "{}" is not supported. Expected the token ' - 'to be signed with "{}"'.format(alg, self._algorithm)) - - kid = header.get('kid', None) - secret_or_certificate = self._fetch_key(key_id=kid) - - try: - decoded = jwt.decode(jwt=token, key=secret_or_certificate, - algorithms=[self._algorithm], options=self.DISABLE_JWT_CHECKS) - except jwt.exceptions.InvalidSignatureError: - raise TokenValidationError("Invalid token signature.") - return decoded - - -class SymmetricSignatureVerifier(SignatureVerifier): - """Verifier for HMAC signatures, which rely on shared secrets. - - Args: - shared_secret (str): The shared secret used to decode the token. - algorithm (str, optional): The expected signing algorithm. Defaults to "HS256". - """ - - def __init__(self, shared_secret, algorithm="HS256"): - super(SymmetricSignatureVerifier, self).__init__(algorithm) - self._shared_secret = shared_secret - - def _fetch_key(self, key_id=None): - return self._shared_secret - - -class AsymmetricSignatureVerifier(SignatureVerifier): - """Verifier for RSA signatures, which rely on public key certificates. - - Args: - jwks_url (str): The url where the JWK set is located. - algorithm (str, optional): The expected signing algorithm. Defaults to "RS256". - """ - - def __init__(self, jwks_url, algorithm="RS256"): - super(AsymmetricSignatureVerifier, self).__init__(algorithm) - self._fetcher = JwksFetcher(jwks_url) - - def _fetch_key(self, key_id=None): - return self._fetcher.get_key(key_id) - - -class JwksFetcher(object): - """Class that fetches and holds a JSON web key set. - This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it. - - Args: - jwks_url (str): The url where the JWK set is located. - cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds. - """ - - CACHE_TTL = 600 # 10 min cache lifetime - - def __init__(self, jwks_url, cache_ttl=CACHE_TTL): - self._jwks_url = jwks_url - self._init_cache(cache_ttl) - return - - def _init_cache(self, cache_ttl): - self._cache_value = {} - self._cache_date = 0 - self._cache_ttl = cache_ttl - self._cache_is_fresh = False - - def _fetch_jwks(self, force=False): - """Attempts to obtain the JWK set from the cache, as long as it's still valid. - When not, it will perform a network request to the jwks_url to obtain a fresh result - and update the cache value with it. - - Args: - force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False. - """ - has_expired = self._cache_date + self._cache_ttl < time.time() - - if not force and not has_expired: - # Return from cache - self._cache_is_fresh = False - return self._cache_value - - # Invalidate cache and fetch fresh data - self._cache_value = {} - response = requests.get(self._jwks_url) - - if response.ok: - # Update cache - jwks = response.json() - self._cache_value = self._parse_jwks(jwks) - self._cache_is_fresh = True - self._cache_date = time.time() - return self._cache_value - - @staticmethod - def _parse_jwks(jwks): - """ - Converts a JWK string representation into a binary certificate in PEM format. - """ - keys = {} - - for key in jwks['keys']: - # noinspection PyUnresolvedReferences - # requirement already includes cryptography -> pyjwt[crypto] - rsa_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key)) - keys[key["kid"]] = rsa_key - return keys - - - def get_key(self, key_id): - """Obtains the JWK associated with the given key id. - - Args: - key_id (str): The id of the key to fetch. - - Returns: - the JWK associated with the given key id. - - Raises: - TokenValidationError: when a key with that id cannot be found - """ - keys = self._fetch_jwks() - - if keys and key_id in keys: - return keys[key_id] - - if not self._cache_is_fresh: - keys = self._fetch_jwks(force=True) - if keys and key_id in keys: - return keys[key_id] - raise TokenValidationError('RSA Public Key with ID "{}" was not found.'.format(key_id)) - - -class TokenVerifier(): - """Class that verifies ID tokens following the steps defined in the OpenID Connect spec. - An OpenID Connect ID token is not meant to be consumed until it's verified. - - Args: - signature_verifier (SignatureVerifier): The instance that knows how to verify the signature. - issuer (str): The expected issuer claim value. - audience (str): The expected audience claim value. - leeway (int, optional): The clock skew to accept when verifying date related claims in seconds. - Defaults to 60 seconds. - """ - - def __init__(self, signature_verifier, issuer, audience, leeway=0): - if not signature_verifier or not isinstance(signature_verifier, SignatureVerifier): - raise TypeError("signature_verifier must be an instance of SignatureVerifier.") - - self.iss = issuer - self.aud = audience - self.leeway = leeway - self._sv = signature_verifier - self._clock = None # visible for testing - - def verify(self, token, nonce=None, max_age=None, organization=None): - """Attempts to verify the given ID token, following the steps defined in the OpenID Connect spec. - - Args: - token (str): The JWT to verify. - nonce (str, optional): The nonce value sent during authentication. - max_age (int, optional): The max_age value sent during authentication. - organization (str, optional): The expected organization ID (org_id) claim value. This should be specified - when logging in to an organization. - - Returns: - the decoded payload from the token - - Raises: - TokenValidationError: when the token cannot be decoded, the token signing algorithm is not the expected one, - the token signature is invalid or the token has a claim missing or with unexpected value. - """ - - # Verify token presence - if not token or not isinstance(token, str): - raise TokenValidationError("ID token is required but missing.") - - # Verify algorithm and signature - payload = self._sv.verify_signature(token) - - # Verify claims - self._verify_payload(payload, nonce, max_age, organization) - - return payload - - def _verify_payload(self, payload, nonce=None, max_age=None, organization=None): - try: - # on Python 2.7, 'str' keys as parsed as 'unicode' - # But 'unicode' was removed on Python 3.7 - # noinspection PyUnresolvedReferences - ustr = unicode - except NameError: - ustr = str - - # Issuer - if 'iss' not in payload or not isinstance(payload['iss'], (str, ustr)): - raise TokenValidationError('Issuer (iss) claim must be a string present in the ID token') - if payload['iss'] != self.iss: - raise TokenValidationError( - 'Issuer (iss) claim mismatch in the ID token; expected "{}", ' - 'found "{}"'.format(self.iss, payload['iss'])) - - # Subject - if 'sub' not in payload or not isinstance(payload['sub'], (str, ustr)): - raise TokenValidationError('Subject (sub) claim must be a string present in the ID token') - - # Audience - if 'aud' not in payload or not isinstance( - payload['aud'], (str, ustr, list) - ): - raise TokenValidationError( - 'Audience (aud) claim must be a string or array of strings present in the ID token') - - if isinstance(payload['aud'], list) and self.aud not in payload['aud']: - payload_audiences = ", ".join(payload['aud']) - raise TokenValidationError( - 'Audience (aud) claim mismatch in the ID token; expected "{}" but was ' - 'not one of "{}"'.format(self.aud, payload_audiences)) - elif isinstance(payload['aud'], (str, ustr)) and payload['aud'] != self.aud: - raise TokenValidationError( - 'Audience (aud) claim mismatch in the ID token; expected "{}" ' - 'but found "{}"'.format(self.aud, payload['aud'])) - - # --Time validation (epoch)-- - now = self._clock or time.time() - leeway = self.leeway - - # Expires at - if 'exp' not in payload or not isinstance(payload['exp'], int): - raise TokenValidationError('Expiration Time (exp) claim must be a number present in the ID token') - - exp_time = payload['exp'] + leeway - if now > exp_time: - raise TokenValidationError( - 'Expiration Time (exp) claim error in the ID token; current time ({}) is ' - 'after expiration time ({})'.format(now, exp_time)) - - # Issued at - if 'iat' not in payload or not isinstance(payload['iat'], int): - raise TokenValidationError('Issued At (iat) claim must be a number present in the ID token') - - # Nonce - if nonce: - if 'nonce' not in payload or not isinstance(payload['nonce'], (str, ustr)): - raise TokenValidationError('Nonce (nonce) claim must be a string present in the ID token') - if payload['nonce'] != nonce: - raise TokenValidationError( - 'Nonce (nonce) claim mismatch in the ID token; expected "{}", ' - 'found "{}"'.format(nonce, payload['nonce'])) - - # Organization - if organization: - if 'org_id' not in payload or not isinstance(payload['org_id'], (str, ustr)): - raise TokenValidationError('Organization (org_id) claim must be a string present in the ID token') - if payload['org_id'] != organization: - raise TokenValidationError( - 'Organization (org_id) claim mismatch in the ID token; expected "{}", ' - 'found "{}"'.format(organization, payload['org_id'])) - - # Authorized party - if isinstance(payload['aud'], list) and len(payload['aud']) > 1: - if 'azp' not in payload or not isinstance(payload['azp'], (str, ustr)): - raise TokenValidationError( - 'Authorized Party (azp) claim must be a string present in the ID token when ' - 'Audience (aud) claim has multiple values') - if payload['azp'] != self.aud: - raise TokenValidationError( - 'Authorized Party (azp) claim mismatch in the ID token; expected "{}", ' - 'found "{}"'.format(self.aud, payload['azp'])) - - # Authentication time - if max_age: - if 'auth_time' not in payload or not isinstance(payload['auth_time'], int): - raise TokenValidationError( - 'Authentication Time (auth_time) claim must be a number present in the ID token ' - 'when Max Age (max_age) is specified') - - auth_valid_until = payload['auth_time'] + max_age + leeway - if now > auth_valid_until: - raise TokenValidationError( - 'Authentication Time (auth_time) claim in the ID token indicates that too much ' - 'time has passed since the last end-user authentication. Current time ({}) ' - 'is after last auth at ({})'.format(now, auth_valid_until)) diff --git a/auth0/v3/authentication/users.py b/auth0/v3/authentication/users.py deleted file mode 100644 index 75733fb5..00000000 --- a/auth0/v3/authentication/users.py +++ /dev/null @@ -1,48 +0,0 @@ -from .base import AuthenticationBase -import warnings - - -class Users(AuthenticationBase): - - """Userinfo related endpoints. - - Args: - domain (str): Your auth0 domain (e.g: username.auth0.com) - """ - - def userinfo(self, access_token): - - """Returns the user information based on the Auth0 access token. - This endpoint will work only if openid was granted as a scope for the access_token. - - Args: - access_token (str): Auth0 access token (obtained during login). - - Returns: - The user profile. - """ - - return self.get( - url='{}://{}/userinfo'.format(self.protocol, self.domain), - headers={'Authorization': 'Bearer {}'.format(access_token)} - ) - - def tokeninfo(self, jwt): - - """Returns user profile based on the user's jwt - - Validates a JSON Web Token (signature and expiration) and returns the - user information associated with the user id (sub property) of - the token. - - Args: - jwt (str): User's jwt - - Returns: - The user profile. - """ - warnings.warn("/tokeninfo will be deprecated in future releases", DeprecationWarning) - return self.post( - url='{}://{}/tokeninfo'.format(self.protocol, self.domain), - data={'id_token': jwt} - ) diff --git a/auth0/v3/exceptions.py b/auth0/v3/exceptions.py deleted file mode 100644 index 789d2a0f..00000000 --- a/auth0/v3/exceptions.py +++ /dev/null @@ -1,18 +0,0 @@ -class Auth0Error(Exception): - def __init__(self, status_code, error_code, message): - self.status_code = status_code - self.error_code = error_code - self.message = message - - def __str__(self): - return '{}: {}'.format(self.status_code, self.message) - - -class RateLimitError(Auth0Error): - def __init__(self, error_code, message, reset_at): - super(RateLimitError, self).__init__(status_code=429, error_code=error_code, message=message) - self.reset_at = reset_at - - -class TokenValidationError(Exception): - pass diff --git a/auth0/v3/management/__init__.py b/auth0/v3/management/__init__.py deleted file mode 100644 index 65d2d87c..00000000 --- a/auth0/v3/management/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from .auth0 import Auth0 -from .actions import Actions -from .attack_protection import AttackProtection -from .blacklists import Blacklists -from .client_grants import ClientGrants -from .clients import Clients -from .connections import Connections -from .custom_domains import CustomDomains -from .device_credentials import DeviceCredentials -from .email_templates import EmailTemplates -from .emails import Emails -from .grants import Grants -from .guardian import Guardian -from .hooks import Hooks -from .jobs import Jobs -from .log_streams import LogStreams -from .logs import Logs -from .organizations import Organizations -from .resource_servers import ResourceServers -from .roles import Roles -from .rules_configs import RulesConfigs -from .rules import Rules -from .stats import Stats -from .tenants import Tenants -from .tickets import Tickets -from .user_blocks import UserBlocks -from .users_by_email import UsersByEmail -from .users import Users diff --git a/auth0/v3/management/auth0.py b/auth0/v3/management/auth0.py deleted file mode 100644 index 55c318e8..00000000 --- a/auth0/v3/management/auth0.py +++ /dev/null @@ -1,73 +0,0 @@ -from .actions import Actions -from .attack_protection import AttackProtection -from .blacklists import Blacklists -from .client_grants import ClientGrants -from .clients import Clients -from .connections import Connections -from .custom_domains import CustomDomains -from .device_credentials import DeviceCredentials -from .email_templates import EmailTemplates -from .emails import Emails -from .grants import Grants -from .guardian import Guardian -from .hooks import Hooks -from .jobs import Jobs -from .log_streams import LogStreams -from .logs import Logs -from .organizations import Organizations -from .prompts import Prompts -from .resource_servers import ResourceServers -from .roles import Roles -from .rules import Rules -from .rules_configs import RulesConfigs -from .stats import Stats -from .tenants import Tenants -from .tickets import Tickets -from .user_blocks import UserBlocks -from .users import Users -from .users_by_email import UsersByEmail - - -class Auth0(object): - """Provides easy access to all endpoint classes - - Args: - domain (str): Your Auth0 domain, e.g: 'username.auth0.com' - - token (str): Management API v2 Token - - rest_options (RestClientOptions): Pass an instance of - RestClientOptions to configure additional RestClient - options, such as rate-limit retries. - (defaults to None) - """ - - def __init__(self, domain, token, rest_options=None): - self.actions = Actions(domain=domain, token=token, rest_options=rest_options) - self.attack_protection = AttackProtection(domain=domain, token=token, rest_options=rest_options) - self.blacklists = Blacklists(domain=domain, token=token, rest_options=rest_options) - self.client_grants = ClientGrants(domain=domain, token=token, rest_options=rest_options) - self.clients = Clients(domain=domain, token=token, rest_options=rest_options) - self.connections = Connections(domain=domain, token=token, rest_options=rest_options) - self.custom_domains = CustomDomains(domain=domain, token=token, rest_options=rest_options) - self.device_credentials = DeviceCredentials(domain=domain, token=token, rest_options=rest_options) - self.email_templates = EmailTemplates(domain=domain, token=token, rest_options=rest_options) - self.emails = Emails(domain=domain, token=token, rest_options=rest_options) - self.grants = Grants(domain=domain, token=token, rest_options=rest_options) - self.guardian = Guardian(domain=domain, token=token, rest_options=rest_options) - self.hooks = Hooks(domain=domain, token=token, rest_options=rest_options) - self.jobs = Jobs(domain=domain, token=token, rest_options=rest_options) - self.log_streams = LogStreams(domain=domain, token=token, rest_options=rest_options) - self.logs = Logs(domain=domain, token=token, rest_options=rest_options) - self.organizations = Organizations(domain=domain, token=token, rest_options=rest_options) - self.prompts = Prompts(domain=domain, token=token, rest_options=rest_options) - self.resource_servers = ResourceServers(domain=domain, token=token, rest_options=rest_options) - self.roles = Roles(domain=domain, token=token, rest_options=rest_options) - self.rules_configs = RulesConfigs(domain=domain, token=token, rest_options=rest_options) - self.rules = Rules(domain=domain, token=token, rest_options=rest_options) - self.stats = Stats(domain=domain, token=token, rest_options=rest_options) - self.tenants = Tenants(domain=domain, token=token, rest_options=rest_options) - self.tickets = Tickets(domain=domain, token=token, rest_options=rest_options) - self.user_blocks = UserBlocks(domain=domain, token=token, rest_options=rest_options) - self.users_by_email = UsersByEmail(domain=domain, token=token, rest_options=rest_options) - self.users = Users(domain=domain, token=token, rest_options=rest_options) diff --git a/auth0/v3/management/client_grants.py b/auth0/v3/management/client_grants.py deleted file mode 100644 index 86499a5a..00000000 --- a/auth0/v3/management/client_grants.py +++ /dev/null @@ -1,101 +0,0 @@ -from .rest import RestClient - - -class ClientGrants(object): - """Auth0 client grants endpoints - - Args: - domain (str): Your Auth0 domain, e.g: 'username.auth0.com' - - token (str): Management API v2 Token - - telemetry (bool, optional): Enable or disable Telemetry - (defaults to True) - - timeout (float or tuple, optional): Change the requests - connect and read timeout. Pass a tuple to specify - both values separately or a float to set both to it. - (defaults to 5.0 for both) - - rest_options (RestClientOptions): Pass an instance of - RestClientOptions to configure additional RestClient - options, such as rate-limit retries. - (defaults to None) - """ - - def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None): - self.domain = domain - self.protocol = protocol - self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options) - - def _url(self, id=None): - url = '{}://{}/api/v2/client-grants'.format(self.protocol, self.domain) - if id is not None: - return '{}/{}'.format(url, id) - return url - - def all(self, audience=None, page=None, per_page=None, include_totals=False, client_id=None): - """Retrieves all client grants. - - Args: - audience (str, optional): URL encoded audience of a Resource Server - to filter. - - page (int, optional): The result's page number (zero based). When not set, - the default value is up to the server. - - per_page (int, optional): The amount of entries per page. When not set, - the default value is up to the server. - - include_totals (bool, optional): True if the query summary is - to be included in the result, False otherwise. Defaults to False. - - client_id (string, optional): The id of a client to filter. - - See: https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants - """ - - params = { - 'audience': audience, - 'page': page, - 'per_page': per_page, - 'include_totals': str(include_totals).lower(), - 'client_id': client_id, - } - - return self.client.get(self._url(), params=params) - - def create(self, body): - """Creates a client grant. - - Args: - body (dict): Attributes for the new client grant. - - See: https://auth0.com/docs/api/management/v2#!/Client_Grants/post_client_grants - """ - - return self.client.post(self._url(), data=body) - - def delete(self, id): - """Deletes a client grant. - - Args: - id (str): Id of client grant to delete. - - See: https://auth0.com/docs/api/management/v2#!/Client_Grants/delete_client_grants_by_id - """ - - return self.client.delete(self._url(id)) - - def update(self, id, body): - """Modifies a client grant. - - Args: - id (str): The id of the client grant to modify. - - body (dict): Attributes to update. - - See: https://auth0.com/docs/api/management/v2#!/Client_Grants/patch_client_grants_by_id - """ - - return self.client.patch(self._url(id), data=body) diff --git a/auth0/v3/management/rest.py b/auth0/v3/management/rest.py deleted file mode 100644 index e8a537e2..00000000 --- a/auth0/v3/management/rest.py +++ /dev/null @@ -1,278 +0,0 @@ -import base64 -import json -import platform -import sys -import requests - -from ..exceptions import Auth0Error, RateLimitError -from time import sleep -from random import randint - -UNKNOWN_ERROR = 'a0.sdk.internal.unknown' - -class RestClientOptions(object): - """Configuration object for RestClient. Used for configuring - additional RestClient options, such as rate-limit - retries. - - Args: - telemetry (bool, optional): Enable or disable Telemetry - (defaults to True) - timeout (float or tuple, optional): Change the requests - connect and read timeout. Pass a tuple to specify - both values separately or a float to set both to it. - (defaults to 5.0 for both) - retries (integer): In the event an API request returns a - 429 response header (indicating rate-limit has been - hit), the RestClient will retry the request this many - times using an exponential backoff strategy, before - raising a RateLimitError exception. 10 retries max. - (defaults to 3) - """ - def __init__(self, telemetry=None, timeout=None, retries=None): - self.telemetry = True - self.timeout = 5.0 - self.retries = 3 - - if telemetry is not None: - self.telemetry = telemetry - - if timeout is not None: - self.timeout = timeout - - if retries is not None: - self.retries = retries - -class RestClient(object): - """Provides simple methods for handling all RESTful api endpoints. - - Args: - telemetry (bool, optional): Enable or disable Telemetry - (defaults to True) - timeout (float or tuple, optional): Change the requests - connect and read timeout. Pass a tuple to specify - both values separately or a float to set both to it. - (defaults to 5.0 for both) - options (RestClientOptions): Pass an instance of - RestClientOptions to configure additional RestClient - options, such as rate-limit retries. Overrides matching - options passed to the constructor. - (defaults to 3) - """ - - def __init__(self, jwt, telemetry=True, timeout=5.0, options=None): - if options is None: - options = RestClientOptions(telemetry=telemetry, timeout=timeout) - - self.options = options - self.jwt = jwt - - self._metrics = {'retries': 0, 'backoff': []} - self._skip_sleep = False - - self.base_headers = { - 'Authorization': 'Bearer {}'.format(self.jwt), - 'Content-Type': 'application/json', - } - - if options.telemetry: - py_version = platform.python_version() - version = sys.modules['auth0'].__version__ - - auth0_client = json.dumps({ - 'name': 'auth0-python', - 'version': version, - 'env': { - 'python': py_version, - } - }).encode('utf-8') - - self.base_headers.update({ - 'User-Agent': 'Python/{}'.format(py_version), - 'Auth0-Client': base64.b64encode(auth0_client), - }) - - # For backwards compatibility reasons only - # TODO: Deprecate in the next major so we can prune these arguments. Guidance should be to use RestClient.options.* - self.telemetry = options.telemetry - self.timeout = options.timeout - - # Returns a hard cap for the maximum number of retries allowed (10) - def MAX_REQUEST_RETRIES(self): - return 10 - - # Returns the maximum amount of jitter to introduce in milliseconds (100ms) - def MAX_REQUEST_RETRY_JITTER(self): - return 100 - - # Returns the maximum delay window allowed (1000ms) - def MAX_REQUEST_RETRY_DELAY(self): - return 1000 - - # Returns the minimum delay window allowed (100ms) - def MIN_REQUEST_RETRY_DELAY(self): - return 100 - - def get(self, url, params=None): - headers = self.base_headers.copy() - - # Track the API request attempt number - attempt = 0 - - # Reset the metrics tracker - self._metrics = {'retries': 0, 'backoff': []} - - # Cap the maximum number of retries to 10 or fewer. Floor the retries at 0. - retries = min(self.MAX_REQUEST_RETRIES(), max(0, self.options.retries)) - - while True: - # Increment attempt number - attempt += 1 - - # Issue the request - response = requests.get(url, params=params, headers=headers, timeout=self.options.timeout); - - # If the response did not have a 429 header, or the retries were configured at 0, or the attempt number is equal to or greater than the configured retries, break - if response.status_code != 429 or retries <= 0 or attempt > retries: - break - - # Retry the request. Apply a exponential backoff for subsequent attempts, using this formula: - # max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (100ms * (2 ** attempt - 1)) + random_between(1, MAX_REQUEST_RETRY_JITTER))) - - # Increases base delay by (100ms * (2 ** attempt - 1)) - wait = 100 * 2 ** (attempt - 1) - - # Introduces jitter to the base delay; increases delay between 1ms to MAX_REQUEST_RETRY_JITTER (100ms) - wait += randint(1, self.MAX_REQUEST_RETRY_JITTER()) - - # Is never more than MAX_REQUEST_RETRY_DELAY (1s) - wait = min(self.MAX_REQUEST_RETRY_DELAY(), wait) - - # Is never less than MIN_REQUEST_RETRY_DELAY (100ms) - wait = max(self.MIN_REQUEST_RETRY_DELAY(), wait) - - self._metrics['retries'] = attempt - self._metrics['backoff'].append(wait) - - # Skip calling sleep() when running unit tests - if self._skip_sleep is False: - # sleep() functions in seconds, so convert the milliseconds formula above accordingly - sleep(wait / 1000) - - # Return the final Response - return self._process_response(response) - - def post(self, url, data=None): - headers = self.base_headers.copy() - - response = requests.post(url, json=data, headers=headers, timeout=self.options.timeout) - return self._process_response(response) - - def file_post(self, url, data=None, files=None): - headers = self.base_headers.copy() - headers.pop('Content-Type', None) - - response = requests.post(url, data=data, files=files, headers=headers, timeout=self.options.timeout) - return self._process_response(response) - - def patch(self, url, data=None): - headers = self.base_headers.copy() - - response = requests.patch(url, json=data, headers=headers, timeout=self.options.timeout) - return self._process_response(response) - - def put(self, url, data=None): - headers = self.base_headers.copy() - - response = requests.put(url, json=data, headers=headers, timeout=self.options.timeout) - return self._process_response(response) - - def delete(self, url, params=None, data=None): - headers = self.base_headers.copy() - - response = requests.delete(url, headers=headers, params=params or {}, json=data, timeout=self.options.timeout) - return self._process_response(response) - - def _process_response(self, response): - return self._parse(response).content() - - def _parse(self, response): - if not response.text: - return EmptyResponse(response.status_code) - try: - return JsonResponse(response) - except ValueError: - return PlainResponse(response) - - -class Response(object): - def __init__(self, status_code, content, headers): - self._status_code = status_code - self._content = content - self._headers = headers - - def content(self): - if self._is_error(): - if self._status_code == 429: - reset_at = int(self._headers.get('x-ratelimit-reset', '-1')) - raise RateLimitError(error_code=self._error_code(), - message=self._error_message(), - reset_at=reset_at) - - raise Auth0Error(status_code=self._status_code, - error_code=self._error_code(), - message=self._error_message()) - else: - return self._content - - def _is_error(self): - return self._status_code is None or self._status_code >= 400 - - # Adding these methods to force implementation in subclasses because they are references in this parent class - def _error_code(self): - raise NotImplementedError - - def _error_message(self): - raise NotImplementedError - - -class JsonResponse(Response): - def __init__(self, response): - content = json.loads(response.text) - super(JsonResponse, self).__init__(response.status_code, content, response.headers) - - def _error_code(self): - if 'errorCode' in self._content: - return self._content.get('errorCode') - elif 'error' in self._content: - return self._content.get('error') - else: - return UNKNOWN_ERROR - - def _error_message(self): - message = self._content.get('message', '') - if message is not None and message != '': - return message - return self._content.get('error', '') - - -class PlainResponse(Response): - def __init__(self, response): - super(PlainResponse, self).__init__(response.status_code, response.text, response.headers) - - def _error_code(self): - return UNKNOWN_ERROR - - def _error_message(self): - return self._content - - -class EmptyResponse(Response): - def __init__(self, status_code): - super(EmptyResponse, self).__init__(status_code, '', {}) - - def _error_code(self): - return UNKNOWN_ERROR - - def _error_message(self): - return '' diff --git a/auth0/v3/test/authentication/test_authorize_client.py b/auth0/v3/test/authentication/test_authorize_client.py deleted file mode 100644 index 59febf3d..00000000 --- a/auth0/v3/test/authentication/test_authorize_client.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -import mock -from ...authentication.authorize_client import AuthorizeClient - - -class TestAuthorizeClient(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.authorize_client.AuthorizeClient.get') - def test_login(self, mock_get): - - a = AuthorizeClient('my.domain.com') - - a.authorize(client_id='cid', - audience='https://test.com/api', - state='st', - redirect_uri='http://localhost', - response_type='token', - scope='openid profile', - organization='org_123', - invitation='invitation_abc') - - args, kwargs = mock_get.call_args - - self.assertEqual(args[0], 'https://my.domain.com/authorize') - self.assertEqual(kwargs['params'], { - 'client_id': 'cid', - 'audience': 'https://test.com/api', - 'state': 'st', - 'redirect_uri': 'http://localhost', - 'response_type': 'token', - 'scope': 'openid profile', - 'organization': 'org_123', - 'invitation': 'invitation_abc' - }) - - @mock.patch('auth0.v3.authentication.authorize_client.AuthorizeClient.get') - def test_login_default_param_values(self, mock_get): - - a = AuthorizeClient('my.domain.com') - - a.authorize(client_id='cid') - - args, kwargs = mock_get.call_args - - self.assertEqual(args[0], 'https://my.domain.com/authorize') - self.assertEqual(kwargs['params'], { - 'audience': None, - 'invitation': None, - 'organization': None, - 'redirect_uri': None, - 'state': None, - 'client_id': 'cid', - 'response_type': 'code', - 'scope': 'openid' - }) diff --git a/auth0/v3/test/authentication/test_base.py b/auth0/v3/test/authentication/test_base.py deleted file mode 100644 index 31b6027e..00000000 --- a/auth0/v3/test/authentication/test_base.py +++ /dev/null @@ -1,279 +0,0 @@ -import base64 -import json -from time import sleep - -import mock -import sys -import requests -import unittest -from ...authentication.base import AuthenticationBase -from ...exceptions import Auth0Error, RateLimitError - - -class TestBase(unittest.TestCase): - - def test_telemetry_enabled_by_default(self): - ab = AuthenticationBase('auth0.com') - - user_agent = ab.base_headers['User-Agent'] - auth0_client_bytes = base64.b64decode(ab.base_headers['Auth0-Client']) - auth0_client_json = auth0_client_bytes.decode('utf-8') - auth0_client = json.loads(auth0_client_json) - content_type = ab.base_headers['Content-Type'] - - from auth0 import __version__ as auth0_version - python_version = '{}.{}.{}'.format(sys.version_info.major, - sys.version_info.minor, - sys.version_info.micro) - - client_info = { - 'name': 'auth0-python', - 'version': auth0_version, - 'env': { - 'python': python_version - } - } - - self.assertEqual(user_agent, 'Python/{}'.format(python_version)) - self.assertEqual(auth0_client, client_info) - self.assertEqual(content_type, 'application/json') - - def test_telemetry_disabled(self): - ab = AuthenticationBase('auth0.com', telemetry=False) - - self.assertEqual(ab.base_headers, {'Content-Type': 'application/json'}) - - @mock.patch('requests.post') - def test_post(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False, timeout=(10, 2)) - - mock_post.return_value.status_code = 200 - mock_post.return_value.text = '{"x": "y"}' - - data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - mock_post.assert_called_with(url='the-url', json={'a': 'b'}, - headers={'c': 'd', 'Content-Type': 'application/json'}, timeout=(10, 2)) - - self.assertEqual(data, {'x': 'y'}) - - @mock.patch('requests.post') - def test_post_with_defaults(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - mock_post.return_value.status_code = 200 - mock_post.return_value.text = '{"x": "y"}' - - # Only required params are passed - data = ab.post('the-url') - - mock_post.assert_called_with(url='the-url', json=None, - headers={'Content-Type': 'application/json'}, timeout=5.0) - - self.assertEqual(data, {'x': 'y'}) - - @mock.patch('requests.post') - def test_post_includes_telemetry(self, mock_post): - ab = AuthenticationBase('auth0.com') - - mock_post.return_value.status_code = 200 - mock_post.return_value.text = '{"x": "y"}' - - data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(mock_post.call_count, 1) - call_kwargs = mock_post.call_args[1] - self.assertEqual(call_kwargs['url'], 'the-url') - self.assertEqual(call_kwargs['json'], {'a': 'b'}) - headers = call_kwargs['headers'] - self.assertEqual(headers['c'], 'd') - self.assertEqual(headers['Content-Type'], 'application/json') - self.assertIn('User-Agent', headers) - self.assertIn('Auth0-Client', headers) - - self.assertEqual(data, {'x': 'y'}) - - @mock.patch('requests.post') - def test_post_error(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = '{"error": "e0",' \ - '"error_description": "desc"}' - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'e0') - self.assertEqual(context.exception.message, 'desc') - - @mock.patch('requests.post') - def test_post_rate_limit_error(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - mock_post.return_value.text = '{"statusCode": 429,' \ - ' "error": "e0",' \ - ' "error_description": "desc"}' - mock_post.return_value.status_code = 429 - mock_post.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'e0') - self.assertEqual(context.exception.message, 'desc') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - @mock.patch('requests.post') - def test_post_rate_limit_error_without_headers(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - mock_post.return_value.text = '{"statusCode": 429,' \ - ' "error": "e0",' \ - ' "error_description": "desc"}' - mock_post.return_value.status_code = 429 - mock_post.return_value.headers = {} - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'e0') - self.assertEqual(context.exception.message, 'desc') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, -1) - - @mock.patch('requests.post') - def test_post_error_with_code_property(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = '{"code": "e0",' \ - '"error_description": "desc"}' - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'e0') - self.assertEqual(context.exception.message, 'desc') - - @mock.patch('requests.post') - def test_post_error_with_no_error_code(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = '{"error_description": "desc"}' - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, - 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, 'desc') - - @mock.patch('requests.post') - def test_post_error_with_text_response(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = 'there has been a terrible error' - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, - 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, - 'there has been a terrible error') - - @mock.patch('requests.post') - def test_post_error_with_no_response_text(self, mock_post): - ab = AuthenticationBase('auth0.com', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = None - - with self.assertRaises(Auth0Error) as context: - ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, - 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, '') - - @mock.patch('requests.get') - def test_get(self, mock_get): - ab = AuthenticationBase('auth0.com', telemetry=False, timeout=(10, 2)) - - mock_get.return_value.status_code = 200 - mock_get.return_value.text = '{"x": "y"}' - - data = ab.get('the-url', params={'a': 'b'}, headers={'c': 'd'}) - - mock_get.assert_called_with(url='the-url', params={'a': 'b'}, - headers={'c': 'd', 'Content-Type': 'application/json'}, timeout=(10, 2)) - - self.assertEqual(data, {'x': 'y'}) - - @mock.patch('requests.get') - def test_get_with_defaults(self, mock_get): - ab = AuthenticationBase('auth0.com', telemetry=False) - - mock_get.return_value.status_code = 200 - mock_get.return_value.text = '{"x": "y"}' - - # Only required params are passed - data = ab.get('the-url') - - mock_get.assert_called_with(url='the-url', params=None, - headers={'Content-Type': 'application/json'}, timeout=5.0) - - self.assertEqual(data, {'x': 'y'}) - - @mock.patch('requests.get') - def test_get_includes_telemetry(self, mock_get): - ab = AuthenticationBase('auth0.com') - - mock_get.return_value.status_code = 200 - mock_get.return_value.text = '{"x": "y"}' - - data = ab.get('the-url', params={'a': 'b'}, headers={'c': 'd'}) - - self.assertEqual(mock_get.call_count, 1) - call_kwargs = mock_get.call_args[1] - self.assertEqual(call_kwargs['url'], 'the-url') - self.assertEqual(call_kwargs['params'], {'a': 'b'}) - headers = call_kwargs['headers'] - self.assertEqual(headers['c'], 'd') - self.assertEqual(headers['Content-Type'], 'application/json') - self.assertIn('User-Agent', headers) - self.assertIn('Auth0-Client', headers) - - self.assertEqual(data, {"x": "y"}) - - def test_get_can_timeout(self): - ab = AuthenticationBase('auth0.com', timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - ab.get('https://google.com', params={'a': 'b'}, headers={'c': 'd'}) - - def test_post_can_timeout(self): - ab = AuthenticationBase('auth0.com', timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - ab.post('https://google.com', data={'a': 'b'}, headers={'c': 'd'}) diff --git a/auth0/v3/test/authentication/test_database.py b/auth0/v3/test/authentication/test_database.py deleted file mode 100644 index d614fd14..00000000 --- a/auth0/v3/test/authentication/test_database.py +++ /dev/null @@ -1,111 +0,0 @@ -import unittest - -import mock - -from ...authentication.database import Database - - -class TestDatabase(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.database.Database.post') - def test_login(self, mock_post): - d = Database('my.domain.com') - - d.login(client_id='cid', - username='usrnm', - password='pswd', - id_token='idt', - connection='conn', - device='dev', - grant_type='gt', - scope='openid profile') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/ro') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'username': 'usrnm', - 'password': 'pswd', - 'id_token': 'idt', - 'connection': 'conn', - 'device': 'dev', - 'grant_type': 'gt', - 'scope': 'openid profile', - }) - - @mock.patch('auth0.v3.authentication.database.Database.post') - def test_signup(self, mock_post): - d = Database('my.domain.com') - - # using only email and password - d.signup(client_id='cid', - email='a@b.com', - password='pswd', - connection='conn') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/dbconnections/signup') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'email': 'a@b.com', - 'password': 'pswd', - 'connection': 'conn', - }) - - # Using also optional properties - sample_meta = { - 'hobby': 'surfing', - 'preference': { - 'color': 'pink' - } - } - d.signup(client_id='cid', - email='a@b.com', - password='pswd', - connection='conn', - username='usr', - user_metadata=sample_meta, - given_name='john', - family_name='doe', - name='john doe', - nickname='johnny', - picture='avatars.com/john-doe') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/dbconnections/signup') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'email': 'a@b.com', - 'password': 'pswd', - 'connection': 'conn', - 'username': 'usr', - 'user_metadata': sample_meta, - 'given_name': 'john', - 'family_name': 'doe', - 'name': 'john doe', - 'nickname': 'johnny', - 'picture': 'avatars.com/john-doe', - }) - - @mock.patch('auth0.v3.authentication.database.Database.post') - def test_change_password(self, mock_post): - d = Database('my.domain.com') - - # ignores the password argument - d.change_password(client_id='cid', - email='a@b.com', - password='pswd', - connection='conn') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], - 'https://my.domain.com/dbconnections/change_password') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'email': 'a@b.com', - 'connection': 'conn', - }) diff --git a/auth0/v3/test/authentication/test_delegated.py b/auth0/v3/test/authentication/test_delegated.py deleted file mode 100644 index a69e346d..00000000 --- a/auth0/v3/test/authentication/test_delegated.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest -import mock -from ...authentication.delegated import Delegated - - -class TestDelegated(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.delegated.Delegated.post') - def test_get_token_id_token(self, mock_post): - - d = Delegated('my.domain.com') - - d.get_token(client_id='cid', - target='tgt', - api_type='apt', - grant_type='gt', - id_token='idt', - scope='openid profile') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/delegation') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'grant_type': 'gt', - 'id_token': 'idt', - 'target': 'tgt', - 'scope': 'openid profile', - 'api_type': 'apt', - }) - - @mock.patch('auth0.v3.authentication.delegated.Delegated.post') - def test_get_token_refresh_token(self, mock_post): - - d = Delegated('my.domain.com') - - d.get_token(client_id='cid', - target='tgt', - api_type='apt', - grant_type='gt', - refresh_token='rtk') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/delegation') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'grant_type': 'gt', - 'refresh_token': 'rtk', - 'target': 'tgt', - 'scope': 'openid', - 'api_type': 'apt', - }) - - @mock.patch('auth0.v3.authentication.delegated.Delegated.post') - def test_get_token_value_error(self, mock_post): - - d = Delegated('my.domain.com') - - with self.assertRaises(ValueError): - d.get_token(client_id='cid', - target='tgt', - api_type='apt', - grant_type='gt', - refresh_token='rtk', - id_token='idt') diff --git a/auth0/v3/test/authentication/test_enterprise.py b/auth0/v3/test/authentication/test_enterprise.py deleted file mode 100644 index bc13ad36..00000000 --- a/auth0/v3/test/authentication/test_enterprise.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -import mock -from ...authentication.enterprise import Enterprise - - -class TestEnterprise(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.enterprise.Enterprise.get') - def test_saml_metadata(self, mock_get): - - e = Enterprise('my.domain.com') - - e.saml_metadata('cid') - - mock_get.assert_called_with( - url='https://my.domain.com/samlp/metadata/cid' - ) - - @mock.patch('auth0.v3.authentication.enterprise.Enterprise.get') - def test_wsfed_metadata(self, mock_get): - - e = Enterprise('my.domain.com') - - e.wsfed_metadata() - - mock_get.assert_called_with( - url='https://my.domain.com/wsfed/FederationMetadata' - '/2007-06/FederationMetadata.xml' - ) diff --git a/auth0/v3/test/authentication/test_get_token.py b/auth0/v3/test/authentication/test_get_token.py deleted file mode 100644 index e5f2f3c4..00000000 --- a/auth0/v3/test/authentication/test_get_token.py +++ /dev/null @@ -1,177 +0,0 @@ -import unittest -import mock -from ...authentication.get_token import GetToken - - -class TestGetToken(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_authorization_code(self, mock_post): - - g = GetToken('my.domain.com') - - g.authorization_code(client_id='cid', - client_secret='clsec', - code='cd', - grant_type='gt', - redirect_uri='idt') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'clsec', - 'code': 'cd', - 'grant_type': 'gt', - 'redirect_uri': 'idt' - }) - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_authorization_code_pkce(self, mock_post): - - g = GetToken('my.domain.com') - - g.authorization_code_pkce(client_id='cid', - code_verifier='cdver', - code='cd', - grant_type='gt', - redirect_uri='idt') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'code_verifier': 'cdver', - 'code': 'cd', - 'grant_type': 'gt', - 'redirect_uri': 'idt' - }) - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_client_credentials(self, mock_post): - - g = GetToken('my.domain.com') - - g.client_credentials(client_id='cid', - client_secret='clsec', - audience='aud', - grant_type='gt') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'clsec', - 'audience': 'aud', - 'grant_type': 'gt' - }) - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_login(self, mock_post): - - g = GetToken('my.domain.com') - - g.login(client_id='cid', - client_secret='clsec', - username='usrnm', - password='pswd', - scope='http://test.com/api', - realm='rlm', - audience='aud', - grant_type='gt') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'clsec', - 'username': 'usrnm', - 'password': 'pswd', - 'scope': 'http://test.com/api', - 'realm': 'rlm', - 'audience': 'aud', - 'grant_type': 'gt' - }) - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_refresh_token(self, mock_post): - g = GetToken('my.domain.com') - - g.refresh_token(client_id='cid', - client_secret='clsec', - refresh_token='rt', - grant_type='gt', - scope='s') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'clsec', - 'refresh_token': 'rt', - 'grant_type': 'gt', - 'scope': 's' - }) - - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_passwordless_login_with_sms(self, mock_post): - - g = GetToken('my.domain.com') - - g.passwordless_login( - client_id='cid', - client_secret='csec', - username='123456', - otp='abcd', - realm='sms', - audience='aud', - scope='openid') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'csec', - 'realm': 'sms', - 'grant_type': 'http://auth0.com/oauth/grant-type/passwordless/otp', - 'username': '123456', - 'otp': 'abcd', - 'audience': 'aud', - 'scope': 'openid', - }) - - - @mock.patch('auth0.v3.authentication.get_token.GetToken.post') - def test_passwordless_login_with_email(self, mock_post): - - g = GetToken('my.domain.com') - - g.passwordless_login( - client_id='cid', - client_secret='csec', - username='a@b.c', - otp='abcd', - realm='email', - audience='aud', - scope='openid') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/token') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'csec', - 'realm': 'email', - 'grant_type': 'http://auth0.com/oauth/grant-type/passwordless/otp', - 'username': 'a@b.c', - 'otp': 'abcd', - 'audience': 'aud', - 'scope': 'openid', - }) diff --git a/auth0/v3/test/authentication/test_logout.py b/auth0/v3/test/authentication/test_logout.py deleted file mode 100644 index 7f8278ee..00000000 --- a/auth0/v3/test/authentication/test_logout.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -import mock -from ...authentication.logout import Logout - - -class TestLogout(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.logout.Logout.get') - def test_logout(self, mock_get): - - g = Logout('my.domain.com') - - g.logout(client_id='cid', - return_to='rto') - - args, kwargs = mock_get.call_args - self.assertEqual(args[0], 'https://my.domain.com/v2/logout?client_id=cid&returnTo=rto') - - @mock.patch('auth0.v3.authentication.logout.Logout.get') - def test_federated_logout(self, mock_get): - - g = Logout('my.domain.com') - - g.logout(client_id='cid', - return_to='rto', - federated=True) - - args, kwargs = mock_get.call_args - self.assertEqual(args[0], 'https://my.domain.com/v2/logout?federated&client_id=cid&returnTo=rto') diff --git a/auth0/v3/test/authentication/test_passwordless.py b/auth0/v3/test/authentication/test_passwordless.py deleted file mode 100644 index 6041f5cf..00000000 --- a/auth0/v3/test/authentication/test_passwordless.py +++ /dev/null @@ -1,137 +0,0 @@ -import unittest -import mock -from ...authentication.passwordless import Passwordless - - -class TestPasswordless(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_email(self, mock_post): - - p = Passwordless('my.domain.com') - - p.email(client_id='cid', - email='a@b.com', - send='snd') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/passwordless/start') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'email': 'a@b.com', - 'send': 'snd', - 'connection': 'email', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_email_with_auth_params(self, mock_post): - - p = Passwordless('my.domain.com') - - p.email(client_id='cid', - email='a@b.com', - send='snd', - auth_params={'a': 'b'}) - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/passwordless/start') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'email': 'a@b.com', - 'send': 'snd', - 'authParams': {'a': 'b'}, - 'connection': 'email', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_email_with_client_secret(self, mock_post): - - p = Passwordless('my.domain.com') - - p.email(client_id='cid', - client_secret='csecret', - email='a@b.com', - send='snd') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/passwordless/start') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'csecret', - 'email': 'a@b.com', - 'send': 'snd', - 'connection': 'email', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_sms(self, mock_post): - p = Passwordless('my.domain.com') - - p.sms(client_id='cid', phone_number='123456') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/passwordless/start') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'phone_number': '123456', - 'connection': 'sms', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_sms_with_client_secret(self, mock_post): - p = Passwordless('my.domain.com') - - p.sms(client_id='cid', client_secret='csecret', phone_number='123456') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/passwordless/start') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'client_secret': 'csecret', - 'phone_number': '123456', - 'connection': 'sms', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_sms_login(self, mock_post): - - p = Passwordless('my.domain.com') - - p.sms_login(client_id='cid', phone_number='123456', code='abcd') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/ro') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'connection': 'sms', - 'grant_type': 'password', - 'username': '123456', - 'password': 'abcd', - 'scope': 'openid', - }) - - @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post') - def test_send_sms_login_with_scope(self, mock_post): - - p = Passwordless('my.domain.com') - - p.sms_login(client_id='cid', phone_number='123456', - code='abcd', scope='openid profile') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/ro') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'connection': 'sms', - 'grant_type': 'password', - 'username': '123456', - 'password': 'abcd', - 'scope': 'openid profile', - }) \ No newline at end of file diff --git a/auth0/v3/test/authentication/test_revoke_token.py b/auth0/v3/test/authentication/test_revoke_token.py deleted file mode 100644 index 57a1d21a..00000000 --- a/auth0/v3/test/authentication/test_revoke_token.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest -import mock -from ...authentication.revoke_token import RevokeToken - - -class TestRevokeToken(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.revoke_token.RevokeToken.post') - def test_revoke_refresh_token(self, mock_post): - - a = RevokeToken('my.domain.com') - - # regular apps - a.revoke_refresh_token(client_id='cid', token='tkn') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/revoke') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'token': 'tkn' - }) - - # confidential apps - a.revoke_refresh_token(client_id='cid', - token='tkn', - client_secret='sh!') - - args, kwargs = mock_post.call_args - - self.assertEqual(args[0], 'https://my.domain.com/oauth/revoke') - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'token': 'tkn', - 'client_secret': 'sh!' - }) diff --git a/auth0/v3/test/authentication/test_social.py b/auth0/v3/test/authentication/test_social.py deleted file mode 100644 index 6874dabf..00000000 --- a/auth0/v3/test/authentication/test_social.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest -import mock -from ...authentication.social import Social - - -class TestSocial(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.social.Social.post') - def test_login(self, mock_post): - s = Social('a.b.c') - s.login(client_id='cid', access_token='atk', connection='conn') - - args, kwargs = mock_post.call_args - - self.assertEqual('https://a.b.c/oauth/access_token', args[0]) - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'access_token': 'atk', - 'connection': 'conn', - 'scope': 'openid', - }) - - @mock.patch('auth0.v3.authentication.social.Social.post') - def test_login_with_scope(self, mock_post): - s = Social('a.b.c') - s.login(client_id='cid', access_token='atk', - connection='conn', scope='openid profile') - - args, kwargs = mock_post.call_args - - self.assertEqual('https://a.b.c/oauth/access_token', args[0]) - self.assertEqual(kwargs['data'], { - 'client_id': 'cid', - 'access_token': 'atk', - 'connection': 'conn', - 'scope': 'openid profile', - }) diff --git a/auth0/v3/test/authentication/test_users.py b/auth0/v3/test/authentication/test_users.py deleted file mode 100644 index b847ebbd..00000000 --- a/auth0/v3/test/authentication/test_users.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest -import mock -from ...authentication.users import Users - - -class TestUsers(unittest.TestCase): - - @mock.patch('auth0.v3.authentication.users.Users.get') - def test_userinfo(self, mock_get): - - u = Users('my.domain.com') - - u.userinfo(access_token='atk') - - mock_get.assert_called_with( - url='https://my.domain.com/userinfo', - headers={'Authorization': 'Bearer atk'} - ) - - @mock.patch('auth0.v3.authentication.users.Users.post') - def test_tokeninfo(self, mock_post): - - u = Users('my.domain.com') - - u.tokeninfo(jwt='jwtoken') - - mock_post.assert_called_with( - url='https://my.domain.com/tokeninfo', - data={'id_token': 'jwtoken'} - ) diff --git a/auth0/v3/test/management/test_actions.py b/auth0/v3/test/management/test_actions.py deleted file mode 100644 index 753474c7..00000000 --- a/auth0/v3/test/management/test_actions.py +++ /dev/null @@ -1,210 +0,0 @@ -import unittest -import mock -from ...management.actions import Actions - - -class TestActions(unittest.TestCase): - - def test_init_with_optionals(self): - t = Actions(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_actions(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_actions() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions', args[0]) - self.assertEqual(kwargs['params'], {'triggerId': None, - 'actionName': None, - 'deployed': 'false', - 'installed': 'false', - 'page': None, - 'per_page': None}) - - c.get_actions('trigger-id', 'action-name', True, True, 0, 5) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions', args[0]) - self.assertEqual(kwargs['params'], {'triggerId': 'trigger-id', - 'actionName': 'action-name', - 'deployed': 'true', - 'installed': 'true', - 'page': 0, - 'per_page': 5}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_create_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.create_action({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/actions/actions', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_update_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.update_action('action-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_action('action-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_triggers(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_triggers() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/triggers', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_delete_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.delete_action('action-id') - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id', args[0]) - self.assertEqual(kwargs['params'], {'force': 'false'}) - - c.delete_action('action-id', True) - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id', args[0]) - self.assertEqual(kwargs['params'], {'force': 'true'}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_execution(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_execution('execution-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/executions/execution-id', args[0]) - self.assertEqual(kwargs['params'], {}) - - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_action_versions(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_action_versions('action-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id/versions', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None}) - - c.get_action_versions('action-id', 0, 5) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id/versions', args[0]) - self.assertEqual(kwargs['params'], {'page': 0, - 'per_page': 5}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_trigger_bindings(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_trigger_bindings('trigger-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/triggers/trigger-id/bindings', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None}) - - c.get_trigger_bindings('trigger-id', 0, 5) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/triggers/trigger-id/bindings', args[0]) - self.assertEqual(kwargs['params'], {'page': 0, - 'per_page': 5}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_get_action_version(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.get_action_version('action-id', 'version-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id/versions/version-id', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_deploy_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.deploy_action('action-id') - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id/deploy', args[0]) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_rollback_action(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.rollback_action_version('action-id', 'version-id') - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/actions/actions/action-id/versions/version-id/deploy', args[0]) - self.assertEqual(kwargs['data'], {}) - - @mock.patch('auth0.v3.management.actions.RestClient') - def test_update_trigger_bindings(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Actions(domain='domain', token='jwttoken') - c.update_trigger_bindings('trigger-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/actions/triggers/trigger-id/bindings', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) diff --git a/auth0/v3/test/management/test_atack_protection.py b/auth0/v3/test/management/test_atack_protection.py deleted file mode 100644 index a299955e..00000000 --- a/auth0/v3/test/management/test_atack_protection.py +++ /dev/null @@ -1,87 +0,0 @@ -import unittest -import mock -from ...management.attack_protection import AttackProtection - - -class TestAttackProtection(unittest.TestCase): - - def test_init_with_optionals(self): - t = AttackProtection(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_get_breached_password_detection(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - ap = AttackProtection(domain='domain', token='jwttoken') - ap.get_breached_password_detection() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/attack-protection/breached-password-detection', args[0]) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_update_breached_password_detection(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.patch.return_value = {} - - c = AttackProtection(domain='domain', token='jwttoken') - c.update_breached_password_detection({'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/attack-protection/breached-password-detection', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_get_brute_force_protection(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - ap = AttackProtection(domain='domain', token='jwttoken') - ap.get_brute_force_protection() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/attack-protection/brute-force-protection', args[0]) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_update_brute_force_protection(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.patch.return_value = {} - - c = AttackProtection(domain='domain', token='jwttoken') - c.update_brute_force_protection({'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/attack-protection/brute-force-protection', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_get_suspicious_ip_throttling(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - ap = AttackProtection(domain='domain', token='jwttoken') - ap.get_suspicious_ip_throttling() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/attack-protection/suspicious-ip-throttling', args[0]) - - @mock.patch('auth0.v3.management.attack_protection.RestClient') - def test_update_suspicious_ip_throttling(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.patch.return_value = {} - - c = AttackProtection(domain='domain', token='jwttoken') - c.update_suspicious_ip_throttling({'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/attack-protection/suspicious-ip-throttling', - data={'a': 'b', 'c': 'd'} - ) diff --git a/auth0/v3/test/management/test_blacklists.py b/auth0/v3/test/management/test_blacklists.py deleted file mode 100644 index 51763082..00000000 --- a/auth0/v3/test/management/test_blacklists.py +++ /dev/null @@ -1,46 +0,0 @@ -import unittest -import mock -from ...management.blacklists import Blacklists - - -class TestBlacklists(unittest.TestCase): - - def test_init_with_optionals(self): - t = Blacklists(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.blacklists.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - t = Blacklists(domain='domain', token='jwttoken') - t.get(aud='an-id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/blacklists/tokens', - params={'aud': 'an-id'} - ) - - @mock.patch('auth0.v3.management.blacklists.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - t = Blacklists(domain='domain', token='jwttoken') - - # create without audience - t.create(jti='the-jti') - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/blacklists/tokens', args[0]) - self.assertEqual(kwargs['data'], {'jti': 'the-jti'}) - - # create with audience - t.create(jti='the-jti', aud='the-aud') - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/blacklists/tokens', args[0]) - self.assertEqual(kwargs['data'], {'jti': 'the-jti', 'aud': 'the-aud'}) diff --git a/auth0/v3/test/management/test_client_grants.py b/auth0/v3/test/management/test_client_grants.py deleted file mode 100644 index d66fe151..00000000 --- a/auth0/v3/test/management/test_client_grants.py +++ /dev/null @@ -1,109 +0,0 @@ -import unittest -import mock -from ...management.client_grants import ClientGrants - - -class TestClientGrants(unittest.TestCase): - - def test_init_with_optionals(self): - t = ClientGrants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.client_grants.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - - c = ClientGrants(domain='domain', token='jwttoken') - - # With default params - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/client-grants', args[0]) - self.assertEqual(kwargs['params'], { - 'audience': None, - 'page': None, - 'per_page': None, - 'include_totals': 'false', - 'client_id': None, - }) - - # With audience - c.all(audience='http://domain.auth0.com/api/v2/') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/client-grants', args[0]) - self.assertEqual(kwargs['params'], { - 'audience': 'http://domain.auth0.com/api/v2/', - 'page': None, - 'per_page': None, - 'include_totals': 'false', - 'client_id': None, - }) - - # With pagination params - c.all(per_page=23, page=7, include_totals=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/client-grants', args[0]) - self.assertEqual(kwargs['params'], { - 'audience': None, - 'page': 7, - 'per_page': 23, - 'include_totals': 'true', - 'client_id': None, - }) - - # With client_id param - c.all(client_id='exampleid') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/client-grants', args[0]) - self.assertEqual(kwargs['params'], { - 'audience': None, - 'page': None, - 'per_page': None, - 'include_totals': 'false', - 'client_id': 'exampleid', - }) - - @mock.patch('auth0.v3.management.client_grants.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = ClientGrants(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/client-grants', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.client_grants.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = ClientGrants(domain='domain', token='jwttoken') - c.delete('this-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/client-grants/this-id' - ) - - @mock.patch('auth0.v3.management.client_grants.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - c = ClientGrants(domain='domain', token='jwttoken') - c.update('this-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/client-grants/this-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) diff --git a/auth0/v3/test/management/test_clients.py b/auth0/v3/test/management/test_clients.py deleted file mode 100644 index b089fd65..00000000 --- a/auth0/v3/test/management/test_clients.py +++ /dev/null @@ -1,133 +0,0 @@ -import unittest -import mock -from ...management.clients import Clients - - -class TestClients(unittest.TestCase): - - def test_init_with_optionals(self): - t = Clients(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'page': None, - 'per_page': None}) - - # Fields filter - c.all(fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false', - 'page': None, - 'per_page': None}) - - # Specific pagination - c.all(page=7, per_page=25) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'page': 7, - 'per_page': 25}) - - # Extra parameters - c.all(extra_params={'some_key': 'some_value'}) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'page': None, - 'per_page': None, - 'some_key': 'some_value'}) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/clients', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - c.get('this-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients/this-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - c.get('this-id', fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/clients/this-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - c.delete('this-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/clients/this-id' - ) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - c.update('this-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/clients/this-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.clients.RestClient') - def test_rotate_secret(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Clients(domain='domain', token='jwttoken') - c.rotate_secret('this-id') - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/clients/this-id/rotate-secret', data={'id': 'this-id'} - ) - - - diff --git a/auth0/v3/test/management/test_connections.py b/auth0/v3/test/management/test_connections.py deleted file mode 100644 index e89d104d..00000000 --- a/auth0/v3/test/management/test_connections.py +++ /dev/null @@ -1,156 +0,0 @@ -import unittest -import mock -from ...management.connections import Connections - - -class TestConnection(unittest.TestCase): - - def test_init_with_optionals(self): - t = Connections(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - # Default parameters are requested - c = Connections(domain='domain', token='jwttoken') - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/connections', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'strategy': None, - 'page': None, - 'per_page': None, - 'include_fields': 'true'}) - - # Fields filter - c.all(fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/connections', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'strategy': None, - 'page': None, - 'per_page': None, - 'include_fields': 'false'}) - - # Fields + strategy filter - c.all(fields=['a', 'b'], strategy='auth0', include_fields=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/connections', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'strategy': 'auth0', - 'page': None, - 'per_page': None, - 'include_fields': 'true'}) - - # Specific pagination - c.all(page=7, per_page=25) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/connections', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'strategy': None, - 'page': 7, - 'per_page': 25, - 'include_fields': 'true'}) - - # Extra parameters - c.all(extra_params={'some_key': 'some_value'}) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/connections', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'strategy': None, - 'page': None, - 'per_page': None, - 'include_fields': 'true', - 'some_key': 'some_value'}) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - c = Connections(domain='domain', token='jwttoken') - c.get('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/connections/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - c.get('an-id', fields=['a', 'b']) - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/connections/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'true'}) - - c.get('an-id', fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/connections/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.delete.return_value = {} - - c = Connections(domain='domain', token='jwttoken') - c.delete('this-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/connections/this-id' - ) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.patch.return_value = {} - - c = Connections(domain='domain', token='jwttoken') - c.update('that-id', {'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/connections/that-id', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.post.return_value = {} - - c = Connections(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/connections', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.connections.RestClient') - def test_delete_user_by_email(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.delete_user_by_email.return_value = {} - - c = Connections(domain='domain', token='jwttoken') - c.delete_user_by_email('123', 'test@example.com') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/connections/123/users', - params={'email': 'test@example.com'}) diff --git a/auth0/v3/test/management/test_custom_domains.py b/auth0/v3/test/management/test_custom_domains.py deleted file mode 100644 index d74e1b69..00000000 --- a/auth0/v3/test/management/test_custom_domains.py +++ /dev/null @@ -1,58 +0,0 @@ -import unittest -import mock -from ...management.custom_domains import CustomDomains - - -class TestCustomDomains(unittest.TestCase): - - def test_init_with_optionals(self): - t = CustomDomains(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.custom_domains.RestClient') - def test_get_all(self, mock_rc): - mock_instance = mock_rc.return_value - - g = CustomDomains(domain='domain', token='jwttoken') - g.all() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/custom-domains' - ) - - @mock.patch('auth0.v3.management.custom_domains.RestClient') - def test_create_new(self, mock_rc): - mock_instance = mock_rc.return_value - - g = CustomDomains(domain='domain', token='jwttoken') - g.create_new(body={'a': 'b', 'c': 'd','e': 'f'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/custom-domains',args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd','e': 'f'}) - - @mock.patch('auth0.v3.management.custom_domains.RestClient') - def test_get_domain_by_id(self, mock_rc): - mock_instance = mock_rc.return_value - - g = CustomDomains(domain='domain', token='jwttoken') - g.get('an-id') - - mock_instance.get.assert_called_with('https://domain/api/v2/custom-domains/an-id') - - - @mock.patch('auth0.v3.management.custom_domains.RestClient') - def test_verify(self, mock_rc): - mock_instance = mock_rc.return_value - - g = CustomDomains(domain='domain', token='jwttoken') - g.verify('an-id') - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/custom-domains/an-id/verify', args[0]) - - diff --git a/auth0/v3/test/management/test_device_credentials.py b/auth0/v3/test/management/test_device_credentials.py deleted file mode 100644 index 036067f3..00000000 --- a/auth0/v3/test/management/test_device_credentials.py +++ /dev/null @@ -1,68 +0,0 @@ -import unittest -import mock -from ...management.device_credentials import DeviceCredentials - - -class TestDeviceCredentials(unittest.TestCase): - - def test_init_with_optionals(self): - t = DeviceCredentials(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.device_credentials.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = DeviceCredentials(domain='domain', token='jwttoken') - c.get(user_id='uid', client_id='cid', type='type', page=0, per_page=20) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/device-credentials', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'user_id': 'uid', - 'client_id': 'cid', - 'type': 'type', - 'page': 0, - 'per_page': 20, - 'include_totals': 'false'}) - - c.get(user_id='uid', client_id='cid', type='type', page=5, per_page=50, include_totals=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/device-credentials', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'user_id': 'uid', - 'client_id': 'cid', - 'type': 'type', - 'page': 5, - 'per_page': 50, - 'include_totals': 'true'}) - - @mock.patch('auth0.v3.management.device_credentials.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = DeviceCredentials(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/device-credentials', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.device_credentials.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = DeviceCredentials(domain='domain', token='jwttoken') - c.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/device-credentials/an-id', - ) diff --git a/auth0/v3/test/management/test_email_endpoints.py b/auth0/v3/test/management/test_email_endpoints.py deleted file mode 100644 index c0aea86d..00000000 --- a/auth0/v3/test/management/test_email_endpoints.py +++ /dev/null @@ -1,47 +0,0 @@ -import unittest -import mock -from ...management.email_templates import EmailTemplates - - -class TestClients(unittest.TestCase): - - def test_init_with_optionals(self): - t = EmailTemplates(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.email_templates.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = EmailTemplates(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/email-templates', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.email_templates.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = EmailTemplates(domain='domain', token='jwttoken') - c.get('this-template-name') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/email-templates/this-template-name' - ) - - @mock.patch('auth0.v3.management.email_templates.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - c = EmailTemplates(domain='domain', token='jwttoken') - c.update('this-template-name', {'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/email-templates/this-template-name', - data={'a': 'b', 'c': 'd'} - ) diff --git a/auth0/v3/test/management/test_emails.py b/auth0/v3/test/management/test_emails.py deleted file mode 100644 index 43222f86..00000000 --- a/auth0/v3/test/management/test_emails.py +++ /dev/null @@ -1,68 +0,0 @@ -import unittest -import mock -from ...management.emails import Emails - - -class TestEmails(unittest.TestCase): - - def test_init_with_optionals(self): - t = Emails(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.emails.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - e = Emails(domain='domain', token='jwttoken') - e.get() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/emails/provider', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - e.get(fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/emails/provider', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - @mock.patch('auth0.v3.management.emails.RestClient') - def test_config(self, mock_rc): - mock_instance = mock_rc.return_value - - e = Emails(domain='domain', token='jwttoken') - e.config({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/emails/provider', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.emails.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - e = Emails(domain='domain', token='jwttoken') - e.update({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/emails/provider', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.emails.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - e = Emails(domain='domain', token='jwttoken') - e.delete() - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/emails/provider' - ) diff --git a/auth0/v3/test/management/test_grants.py b/auth0/v3/test/management/test_grants.py deleted file mode 100644 index f818fe92..00000000 --- a/auth0/v3/test/management/test_grants.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest -import mock -from ...management.grants import Grants - - -class TestGrants(unittest.TestCase): - - def test_init_with_optionals(self): - t = Grants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.grants.RestClient') - def test_get_all(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Grants(domain='domain', token='jwttoken') - g.all(extra_params={'user_id':'an-id', 'client_id': 'an-id', 'audience':'test'}) - - args, kwargs = mock_instance.get.call_args - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/grants', params={'user_id': 'an-id', 'client_id': 'an-id', 'audience': 'test', 'page': None, 'per_page': None, 'include_totals': 'false'} - ) - - - @mock.patch('auth0.v3.management.grants.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Grants(domain='domain', token='jwttoken') - c.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/grants/an-id' - ) diff --git a/auth0/v3/test/management/test_guardian.py b/auth0/v3/test/management/test_guardian.py deleted file mode 100644 index c3656adf..00000000 --- a/auth0/v3/test/management/test_guardian.py +++ /dev/null @@ -1,131 +0,0 @@ -import unittest -import mock -from ...management.guardian import Guardian - - -class TestGuardian(unittest.TestCase): - - def test_init_with_optionals(self): - t = Guardian(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_all_factors(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.all_factors() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/guardian/factors' - ) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_update_factor(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.update_factor('push-notification', {'enabled': True}) - - args, kwargs = mock_instance.put.call_args - self.assertEqual('https://domain/api/v2/guardian/factors/push-notification', args[0]) - self.assertEqual(kwargs['data'], {'enabled': True}) - - g.update_factor('sms', {'enabled': False}) - - args, kwargs = mock_instance.put.call_args - self.assertEqual('https://domain/api/v2/guardian/factors/sms', args[0]) - self.assertEqual(kwargs['data'], {'enabled': False}) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_update_templates(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.update_templates({'enrollment_message': 'hello', - 'verification_message': 'verified'}) - - args, kwargs = mock_instance.put.call_args - self.assertEqual('https://domain/api/v2/guardian/factors/sms/templates', args[0]) - self.assertEqual(kwargs['data'], {'enrollment_message': 'hello', - 'verification_message': 'verified'}) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_get_templates(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.get_templates() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/guardian/factors/sms/templates' - ) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_get_enrollment(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.get_enrollment('some_id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/guardian/enrollments/some_id' - ) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_delete_enrollment(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.delete_enrollment('some_id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/guardian/enrollments/some_id' - ) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_create_enrollment_ticket(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.create_enrollment_ticket({'user_id': 'some_id', - 'email': 'test@test.com', - 'send_mail': 'false'}) - - args, kwargs = mock_instance.post.call_args - self.assertEqual('https://domain/api/v2/guardian/enrollments/ticket', args[0]) - self.assertEqual(kwargs['data'], {'user_id': 'some_id', - 'email': 'test@test.com', - 'send_mail': 'false'}) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_get_factor_providers(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.get_factor_providers('sms', 'twilio') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/guardian/factors/sms/providers/twilio' - ) - - @mock.patch('auth0.v3.management.guardian.RestClient') - def test_update_factor_providers(self, mock_rc): - mock_instance = mock_rc.return_value - - g = Guardian(domain='domain', token='jwttoken') - g.update_factor_providers('sms', - 'twilio', - {'from': 'test@test.com', - 'messaging_service_sid': 'qwerty', - 'auth_token': 'abc.xyz.123', - 'sid': 'abc.xyz'}) - - args, kwargs = mock_instance.put.call_args - self.assertEqual('https://domain/api/v2/guardian/factors/sms/providers/twilio', args[0]) - self.assertEqual(kwargs['data'], {'from': 'test@test.com', - 'messaging_service_sid': 'qwerty', - 'auth_token': 'abc.xyz.123', - 'sid': 'abc.xyz'}) diff --git a/auth0/v3/test/management/test_hooks.py b/auth0/v3/test/management/test_hooks.py deleted file mode 100644 index 9137ebaa..00000000 --- a/auth0/v3/test/management/test_hooks.py +++ /dev/null @@ -1,163 +0,0 @@ -import unittest -import mock -from ...management.hooks import Hooks - - -class TestRules(unittest.TestCase): - - def test_init_with_optionals(self): - t = Hooks(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - - # with default params - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks', args[0]) - self.assertEqual(kwargs['params'], { - "enabled": 'true', - "fields": None, - "include_fields": 'true', - "page": None, - "per_page": None, - "include_totals": 'false', - }) - - # with fields params - c.all(enabled=False, fields=['a', 'b'], - include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false', - 'enabled': 'false', - 'page': None, - 'per_page': None, - 'include_totals': 'false'}) - - # with pagination params - c.all(page=3, per_page=27, include_totals=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'enabled': 'true', - 'page': 3, - 'per_page': 27, - 'include_totals': 'true'}) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/hooks', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.get('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': None}) - - c.get('an-id', fields=['a', 'b']) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b'}) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/hooks/an-id' - ) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.update('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - # test for hooks secrets - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_add_secret(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.add_secrets('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id/secrets', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_get_secrets(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.get_secrets('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id/secrets', args[0]) - self.assertNotIn("params", kwargs) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_delete_secrets(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.delete_secrets('an-id', ['a', 'b']) - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id/secrets', args[0]) - self.assertEqual(kwargs['data'], ['a', 'b']) - - @mock.patch('auth0.v3.management.hooks.RestClient') - def test_update_secrets(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Hooks(domain='domain', token='jwttoken') - c.update_secrets('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/hooks/an-id/secrets', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) diff --git a/auth0/v3/test/management/test_jobs.py b/auth0/v3/test/management/test_jobs.py deleted file mode 100644 index 1a3ba268..00000000 --- a/auth0/v3/test/management/test_jobs.py +++ /dev/null @@ -1,97 +0,0 @@ -import unittest -import mock -from ...management.jobs import Jobs - - -class TestJobs(unittest.TestCase): - - def test_init_with_optionals(self): - t = Jobs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.get('an-id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/jobs/an-id', - ) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_get_failed_job(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.get_failed_job('an-id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/jobs/an-id/errors', - ) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_get_job_results(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.get_results('an-id') - - # Should use the 'get by id' URL - mock_instance.get.assert_called_with( - 'https://domain/api/v2/jobs/an-id', - ) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_export_users(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.export_users({'connection_id': 'cxn_id', 'format': 'json'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/jobs/users-exports', - data={'connection_id': 'cxn_id', 'format': 'json'} - ) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_import_users(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.import_users(connection_id='1234', file_obj={}) - - mock_instance.file_post.assert_called_with( - 'https://domain/api/v2/jobs/users-imports', - data={'connection_id': '1234', 'upsert': 'false', 'send_completion_email': 'true', 'external_id': None}, - files={'users': {}} - ) - - j.import_users(connection_id='1234', file_obj={}, upsert=True, send_completion_email=False, external_id="ext-id-123") - mock_instance.file_post.assert_called_with( - 'https://domain/api/v2/jobs/users-imports', - data={'connection_id': '1234', 'upsert': 'true', 'send_completion_email': 'false', 'external_id': 'ext-id-123'}, - files={'users': {}} - ) - - j.import_users(connection_id='1234', file_obj={}, upsert=False, send_completion_email=True) - mock_instance.file_post.assert_called_with( - 'https://domain/api/v2/jobs/users-imports', - data={'connection_id': '1234', 'upsert': 'false', 'send_completion_email': 'true', 'external_id': None}, - files={'users': {}} - ) - - @mock.patch('auth0.v3.management.jobs.RestClient') - def test_verification_email(self, mock_rc): - mock_instance = mock_rc.return_value - - j = Jobs(domain='domain', token='jwttoken') - j.send_verification_email({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/jobs/verification-email', - data={'a': 'b', 'c': 'd'} - ) diff --git a/auth0/v3/test/management/test_log_streams.py b/auth0/v3/test/management/test_log_streams.py deleted file mode 100644 index 42f1d007..00000000 --- a/auth0/v3/test/management/test_log_streams.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest - -import mock - -from ...management.log_streams import LogStreams - - -class TestLogStreams(unittest.TestCase): - - def test_init_with_optionals(self): - t = LogStreams(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.log_streams.RestClient') - def test_list(self, mock_rc): - mock_instance = mock_rc.return_value - - c = LogStreams(domain='domain', token='jwttoken') - - c.list() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/log-streams', args[0]) - - @mock.patch('auth0.v3.management.log_streams.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = LogStreams(domain='domain', token='jwttoken') - c.get('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/log-streams/an-id', args[0]) - - @mock.patch('auth0.v3.management.log_streams.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = LogStreams(domain='domain', token='jwttoken') - # Sample data belongs to an `http` stream - log_stream_data = { - "name": "string", - "type": "http", - "sink": { - "httpEndpoint": "string", - "httpContentType": "string", - "httpContentFormat": "JSONLINES|JSONARRAY", - "httpAuthorization": "string" - } - } - c.create(log_stream_data) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/log-streams', args[0]) - self.assertEqual(kwargs['data'], log_stream_data) - - @mock.patch('auth0.v3.management.log_streams.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = LogStreams(domain='domain', token='jwttoken') - c.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/log-streams/an-id' - ) - - @mock.patch('auth0.v3.management.log_streams.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - log_stream_update = { - "name": "string" - } - - c = LogStreams(domain='domain', token='jwttoken') - c.update('an-id', log_stream_update) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/log-streams/an-id', args[0]) - self.assertEqual(kwargs['data'], log_stream_update) diff --git a/auth0/v3/test/management/test_logs.py b/auth0/v3/test/management/test_logs.py deleted file mode 100644 index 84acdf4e..00000000 --- a/auth0/v3/test/management/test_logs.py +++ /dev/null @@ -1,53 +0,0 @@ -import unittest -import mock -from ...management.logs import Logs - - -class TestLogs(unittest.TestCase): - - def test_init_with_optionals(self): - t = Logs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.logs.RestClient') - def test_search(self, mock_rc): - mock_instance = mock_rc.return_value - - logs = Logs(domain='domain', token='jwttoken') - logs.search() - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/logs', args[0]) - self.assertIsNone(kwargs['params']['sort']) - self.assertIsNone(kwargs['params']['q']) - self.assertIsNone(kwargs['params']['from']) - self.assertIsNone(kwargs['params']['take']) - self.assertEqual(kwargs['params']['include_fields'], 'true') - self.assertEqual(kwargs['params']['include_totals'], 'true') - self.assertEqual(kwargs['params']['per_page'], 50) - self.assertEqual(kwargs['params']['page'], 0) - self.assertIsNone(kwargs['params']['fields']) - - logs.search(fields=['description', 'client_id']) - - args, kwargs = mock_instance.get.call_args - self.assertEqual(kwargs['params']['fields'], 'description,client_id') - - logs.search(page=0, per_page=2) - - args, kwargs = mock_instance.get.call_args - self.assertEqual(kwargs['params']['per_page'], 2) - self.assertEqual(kwargs['params']['page'], 0) - - @mock.patch('auth0.v3.management.logs.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - logs = Logs(domain='domain', token='jwttoken') - logs.get('get_id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/logs/get_id' - ) diff --git a/auth0/v3/test/management/test_organizations.py b/auth0/v3/test/management/test_organizations.py deleted file mode 100644 index 192016e2..00000000 --- a/auth0/v3/test/management/test_organizations.py +++ /dev/null @@ -1,362 +0,0 @@ -import unittest -import mock -from ...management.organizations import Organizations - - -class TestOrganizations(unittest.TestCase): - - def test_init_with_optionals(self): - t = Organizations(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - # Organizations - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_all_organizations(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all_organizations() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None, - 'include_totals': 'true', - 'from': None, - 'take': None}) - - # Basic pagination - c.all_organizations(page=7, per_page=25, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations', args[0]) - self.assertEqual(kwargs['params'], {'page': 7, - 'per_page': 25, - 'include_totals': 'false', - 'from': None, - 'take': None}) - - # Checkpoint pagination - c.all_organizations(from_param=8675309, take=75) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations', args[0]) - self.assertEqual(kwargs['params'], {'from': 8675309, - 'take': 75, - 'page': None, - 'per_page': None, - 'include_totals': 'true'}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_get_organization_by_name(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.get_organization_by_name('test-org') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/name/test-org', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_get_organization(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.get_organization('org123') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/org123', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_create_organization(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.create_organization({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/organizations', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_update_organization(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.update_organization('this-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/organizations/this-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_delete_organization(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.delete_organization('this-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/organizations/this-id' - ) - - # Organization Connections - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_all_organization_connections(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all_organization_connections('test-org') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/enabled_connections', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None}) - - # Specific pagination - c.all_organization_connections('test-org', page=7, per_page=25) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/enabled_connections', args[0]) - self.assertEqual(kwargs['params'], {'page': 7, - 'per_page': 25}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_get_organization_connection(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.get_organization_connection('test-org', 'test-con') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/enabled_connections/test-con', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_create_organization_connection(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.create_organization_connection('test-org', {'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/organizations/test-org/enabled_connections', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_update_organization_connection(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.update_organization_connection('test-org', 'test-con', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/enabled_connections/test-con', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_delete_organization_connection(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.delete_organization_connection('test-org', 'test-con') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/organizations/test-org/enabled_connections/test-con' - ) - - # Organization Members - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_all_organization_members(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all_organization_members('test-org') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/members', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None, - 'include_totals': 'true', - 'from': None, - 'take': None}) - - # Specific pagination - c.all_organization_members('test-org', page=7, per_page=25, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/members', args[0]) - self.assertEqual(kwargs['params'], {'page': 7, - 'per_page': 25, - 'include_totals': 'false', - 'from': None, - 'take': None}) - - # Checkpoint pagination - c.all_organization_members('test-org', from_param=8675309, take=75) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/members', args[0]) - self.assertEqual(kwargs['params'], {'from': 8675309, - 'take': 75, - 'page': None, - 'per_page': None, - 'include_totals': 'true'}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_create_organization_members(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.create_organization_members('test-org', {'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/organizations/test-org/members', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_delete_organization_members(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.delete_organization_members('test-org', {'a': 'b', 'c': 'd'}) - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/organizations/test-org/members', - data={'a': 'b', 'c': 'd'} - ) - - # Organization Member Roles - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_all_organization_member_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all_organization_member_roles('test-org', 'test-user') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/members/test-user/roles', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None}) - - # Specific pagination - c.all_organization_member_roles('test-org', 'test-user', page=7, per_page=25) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/members/test-user/roles', args[0]) - self.assertEqual(kwargs['params'], {'page': 7, - 'per_page': 25}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_create_organization_member_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.create_organization_member_roles('test-org', 'test-user', {'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/organizations/test-org/members/test-user/roles', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_delete_organization_member_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.delete_organization_member_roles('test-org', 'test-user', {'a': 'b', 'c': 'd'}) - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/organizations/test-org/members/test-user/roles', - data={'a': 'b', 'c': 'd'} - ) - - # Organization Invitations - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_all_organization_invitations(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - - # Default parameters are requested - c.all_organization_invitations('test-org') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/invitations', args[0]) - self.assertEqual(kwargs['params'], {'page': None, - 'per_page': None}) - - # Specific pagination - c.all_organization_invitations('test-org', page=7, per_page=25) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/invitations', args[0]) - self.assertEqual(kwargs['params'], {'page': 7, - 'per_page': 25}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_get_organization_invitation(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.get_organization_invitation('test-org', 'test-inv') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/organizations/test-org/invitations/test-inv', args[0]) - self.assertEqual(kwargs['params'], {}) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_create_organization_invitation(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.create_organization_invitation('test-org', {'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/organizations/test-org/invitations', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.organizations.RestClient') - def test_delete_organization_invitation(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Organizations(domain='domain', token='jwttoken') - c.delete_organization_invitation('test-org', 'test-inv') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/organizations/test-org/invitations/test-inv' - ) diff --git a/auth0/v3/test/management/test_resource_servers.py b/auth0/v3/test/management/test_resource_servers.py deleted file mode 100644 index 911bf238..00000000 --- a/auth0/v3/test/management/test_resource_servers.py +++ /dev/null @@ -1,94 +0,0 @@ -import unittest -import mock -from ...management.resource_servers import ResourceServers - - -class TestResourceServers(unittest.TestCase): - - def test_init_with_optionals(self): - t = ResourceServers(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.resource_servers.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - r = ResourceServers(domain='domain', token='jwttoken') - - r.create({'name': 'TestApi', 'identifier': 'https://test.com/api'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/resource-servers', - data={'name': 'TestApi', 'identifier': 'https://test.com/api'} - ) - - @mock.patch('auth0.v3.management.resource_servers.RestClient') - def test_get_all(self, mock_rc): - mock_instance = mock_rc.return_value - - r = ResourceServers(domain='domain', token='jwttoken') - - # with default params - r.get_all() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/resource-servers', - params={ - 'page': None, - 'per_page': None, - 'include_totals': 'false' - } - ) - - # with pagination params - r.get_all(page=3, per_page=27, include_totals=True) - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/resource-servers', - params={ - 'page': 3, - 'per_page': 27, - 'include_totals': 'true' - } - ) - - @mock.patch('auth0.v3.management.resource_servers.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - r = ResourceServers(domain='domain', token='jwttoken') - - r.get('some_id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/resource-servers/some_id' - ) - - @mock.patch('auth0.v3.management.resource_servers.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - r = ResourceServers(domain='domain', token='jwttoken') - - r.delete('some_id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/resource-servers/some_id' - ) - - @mock.patch('auth0.v3.management.resource_servers.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - r = ResourceServers(domain='domain', token='jwttoken') - - r.update('some_id', {'name': 'TestApi2', - 'identifier': 'https://test.com/api2'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/resource-servers/some_id', - data={'name': 'TestApi2', - 'identifier': 'https://test.com/api2'} - ) diff --git a/auth0/v3/test/management/test_rest.py b/auth0/v3/test/management/test_rest.py deleted file mode 100644 index 78d44149..00000000 --- a/auth0/v3/test/management/test_rest.py +++ /dev/null @@ -1,769 +0,0 @@ -import unittest -import sys -import json -import base64 - -import mock -import requests - -from ...management.rest import RestClient, RestClientOptions -from ...exceptions import Auth0Error, RateLimitError - - -class TestRest(unittest.TestCase): - def test_options_are_used_and_override(self): - """ - This test ensures RestClientOptions are read when passed to - RestClient's constructor by (1) configuring a timeout and (2) - turning off Telemetry. This proves that RestClient is inheriting - those options, and overriding it's own constructor arguments. - """ - - options = RestClientOptions(telemetry=False, timeout=0.00001, retries=10) - rc = RestClient(jwt='a-token', telemetry=True, timeout=30, options=options) - - # Does a timeout occur as expected? - with self.assertRaises(requests.exceptions.Timeout): - rc.get('http://google.com') - - # Is RestClient using the RestClientOptions.timeout value properly? - self.assertEqual(rc.options.timeout, 0.00001) - - # Is RestClient using the RestClientOptions.retries value properly? - self.assertEqual(rc.options.retries, 10) - - # Is RestClient using the RestClientOptions.telemetry value properly? - self.assertEqual(rc.options.telemetry, False) - - # Is RestClient using the RestClientOptions.telemetry value properly? - self.assertEqual(rc.base_headers, { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer a-token', - }) - - def test_options_are_created_by_default(self): - """ - This test ensures RestClientOptions are read when passed to - RestClient's constructor by (1) configuring a timeout and (2) - turning off Telemetry. This proves that RestClient is inheriting - those options, and overriding it's own constructor arguments. - """ - - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00002) - - # Does a timeout occur as expected? - with self.assertRaises(requests.exceptions.Timeout): - rc.get('http://google.com') - - # Did RestClient create a RestClientOptions for us? - self.assertIsNotNone(rc.options) - - # Did RestClient assign the new RestClientOptions instance the proper timeout value from the constructor? - self.assertEqual(rc.options.timeout, 0.00002) - - # Did RestClient use the default RestClientOptions value for retries? - self.assertEqual(rc.options.retries, 3) - - # Did RestClient assign the new RestClientOptions instance the proper telemetry value from the constructor? - self.assertEqual(rc.options.telemetry, False) - - # Is RestClient using the RestClientOptions.telemetry value properly? - self.assertEqual(rc.base_headers, { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer a-token', - }) - - def test_default_options_are_used(self): - """ - This test ensures RestClientOptions are read when passed to - RestClient's constructor by (1) configuring a timeout and (2) - turning off Telemetry. This proves that RestClient is inheriting - those options, and overriding it's own constructor arguments. - """ - - options = RestClientOptions() - rc = RestClient(jwt='a-token', options=options) - - # Did RestClient store the RestClientOptions? - self.assertIsNotNone(rc.options) - - # Did RestClientOptions use the default 5.0 timeout? - self.assertEqual(rc.options.timeout, 5.0) - - # Did RestClientOptions use the default 3 retries? - self.assertEqual(rc.options.retries, 3) - - # Did RestClientOptions use the default True telemetry value? - self.assertEqual(rc.options.telemetry, True) - - def test_get_can_timeout(self): - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - rc.get('http://google.com') - - def test_post_can_timeout(self): - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - rc.post('http://google.com') - - def test_put_can_timeout(self): - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - rc.put('http://google.com') - - def test_patch_can_timeout(self): - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - rc.patch('http://google.com') - - def test_delete_can_timeout(self): - rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001) - - with self.assertRaises(requests.exceptions.Timeout): - rc.delete('http://google.com') - - @mock.patch('requests.get') - def test_get_custom_timeout(self, mock_get): - rc = RestClient(jwt='a-token', telemetry=False, timeout=(10, 2)) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - mock_get.return_value.text = '["a", "b"]' - mock_get.return_value.status_code = 200 - - rc.get('the-url') - mock_get.assert_called_with('the-url', params=None, headers=headers, timeout=(10, 2)) - - @mock.patch('requests.post') - def test_post_custom_timeout(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False, timeout=(10, 2)) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - mock_post.return_value.text = '["a", "b"]' - mock_post.return_value.status_code = 200 - - rc.post('the-url') - mock_post.assert_called_with('the-url', json=None, headers=headers, timeout=(10, 2)) - - @mock.patch('requests.put') - def test_put_custom_timeout(self, mock_put): - rc = RestClient(jwt='a-token', telemetry=False, timeout=(10, 2)) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - mock_put.return_value.text = '["a", "b"]' - mock_put.return_value.status_code = 200 - - rc.put('the-url') - mock_put.assert_called_with('the-url', json=None, headers=headers, timeout=(10, 2)) - - @mock.patch('requests.patch') - def test_patch_custom_timeout(self, mock_patch): - rc = RestClient(jwt='a-token', telemetry=False, timeout=(10, 2)) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - mock_patch.return_value.text = '["a", "b"]' - mock_patch.return_value.status_code = 200 - - rc.patch('the-url') - mock_patch.assert_called_with('the-url', json=None, headers=headers, timeout=(10, 2)) - - @mock.patch('requests.delete') - def test_delete_custom_timeout(self, mock_delete): - rc = RestClient(jwt='a-token', telemetry=False, timeout=(10, 2)) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - mock_delete.return_value.text = '["a", "b"]' - mock_delete.return_value.status_code = 200 - - rc.delete('the-url') - mock_delete.assert_called_with('the-url', params={}, json=None, headers=headers, timeout=(10, 2)) - - @mock.patch('requests.get') - def test_get(self, mock_get): - rc = RestClient(jwt='a-token', telemetry=False) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - - mock_get.return_value.text = '["a", "b"]' - mock_get.return_value.status_code = 200 - - response = rc.get('the-url') - mock_get.assert_called_with('the-url', params=None, headers=headers, timeout=5.0) - - self.assertEqual(response, ['a', 'b']) - - response = rc.get(url='the/url', params={'A': 'param', 'B': 'param'}) - mock_get.assert_called_with('the/url', params={'A': 'param', - 'B': 'param'}, - headers=headers, timeout=5.0) - self.assertEqual(response, ['a', 'b']) - - mock_get.return_value.text = '' - response = rc.get('the/url') - self.assertEqual(response, '') - - @mock.patch('requests.get') - def test_get_errors(self, mock_get): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_get.return_value.text = '{"statusCode": 999,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.get('the/url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - @mock.patch('requests.get') - def test_get_rate_limit_error(self, mock_get): - options = RestClientOptions(telemetry=False, retries=0) - rc = RestClient(jwt='a-token', options=options) - rc._skip_sleep = True - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - mock_get.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - self.assertEqual(rc._metrics['retries'], 0) - - @mock.patch('requests.get') - def test_get_rate_limit_error_without_headers(self, mock_get): - options = RestClientOptions(telemetry=False, retries=1) - rc = RestClient(jwt='a-token', options=options) - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - - mock_get.return_value.headers = {} - with self.assertRaises(Auth0Error) as context: - rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, -1) - - self.assertEqual(rc._metrics['retries'], 1) - - @mock.patch('requests.get') - def test_get_rate_limit_custom_retries(self, mock_get): - options = RestClientOptions(telemetry=False, retries=5) - rc = RestClient(jwt='a-token', options=options) - rc._skip_sleep = True - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - mock_get.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - response = rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - self.assertEqual(rc._metrics['retries'], 5) - self.assertEqual(rc._metrics['retries'], len(rc._metrics['backoff'])) - - @mock.patch('requests.get') - def test_get_rate_limit_invalid_retries_below_min(self, mock_get): - options = RestClientOptions(telemetry=False, retries=-1) - rc = RestClient(jwt='a-token', options=options) - rc._skip_sleep = True - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - mock_get.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - response = rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - self.assertEqual(rc._metrics['retries'], 0) - - - @mock.patch('requests.get') - def test_get_rate_limit_invalid_retries_above_max(self, mock_get): - options = RestClientOptions(telemetry=False, retries=11) - rc = RestClient(jwt='a-token', options=options) - rc._skip_sleep = True - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - mock_get.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - response = rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - self.assertEqual(rc._metrics['retries'], rc.MAX_REQUEST_RETRIES()) - - @mock.patch('requests.get') - def test_get_rate_limit_retries_use_exponential_backoff(self, mock_get): - options = RestClientOptions(telemetry=False, retries=10) - rc = RestClient(jwt='a-token', options=options) - rc._skip_sleep = True - - mock_get.return_value.text = '{"statusCode": 429,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_get.return_value.status_code = 429 - mock_get.return_value.headers = { - 'x-ratelimit-limit': '3', - 'x-ratelimit-remaining': '6', - 'x-ratelimit-reset': '9', - } - - with self.assertRaises(Auth0Error) as context: - response = rc.get('the/url') - - self.assertEqual(context.exception.status_code, 429) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - self.assertIsInstance(context.exception, RateLimitError) - self.assertEqual(context.exception.reset_at, 9) - - self.assertEqual(rc._metrics['retries'], 10) - self.assertEqual(rc._metrics['retries'], len(rc._metrics['backoff'])) - - baseBackoff = [0] - baseBackoffSum = 0 - finalBackoff = 0 - - for i in range(0, 9): - backoff = 100 * 2 ** i - baseBackoff.append(backoff) - baseBackoffSum += backoff - - for backoff in rc._metrics['backoff']: - finalBackoff += backoff - - # Assert that exponential backoff is happening. - self.assertGreaterEqual(rc._metrics['backoff'][1], rc._metrics['backoff'][0]) - self.assertGreaterEqual(rc._metrics['backoff'][2], rc._metrics['backoff'][1]) - self.assertGreaterEqual(rc._metrics['backoff'][3], rc._metrics['backoff'][2]) - self.assertGreaterEqual(rc._metrics['backoff'][4], rc._metrics['backoff'][3]) - self.assertGreaterEqual(rc._metrics['backoff'][5], rc._metrics['backoff'][4]) - self.assertGreaterEqual(rc._metrics['backoff'][6], rc._metrics['backoff'][5]) - self.assertGreaterEqual(rc._metrics['backoff'][7], rc._metrics['backoff'][6]) - self.assertGreaterEqual(rc._metrics['backoff'][8], rc._metrics['backoff'][7]) - self.assertGreaterEqual(rc._metrics['backoff'][9], rc._metrics['backoff'][8]) - - # Ensure jitter is being applied. - self.assertNotEqual(rc._metrics['backoff'][1], baseBackoff[1]) - self.assertNotEqual(rc._metrics['backoff'][2], baseBackoff[2]) - self.assertNotEqual(rc._metrics['backoff'][3], baseBackoff[3]) - self.assertNotEqual(rc._metrics['backoff'][4], baseBackoff[4]) - self.assertNotEqual(rc._metrics['backoff'][5], baseBackoff[5]) - self.assertNotEqual(rc._metrics['backoff'][6], baseBackoff[6]) - self.assertNotEqual(rc._metrics['backoff'][7], baseBackoff[7]) - self.assertNotEqual(rc._metrics['backoff'][8], baseBackoff[8]) - self.assertNotEqual(rc._metrics['backoff'][9], baseBackoff[9]) - - # Ensure subsequent delay is never less than the minimum. - self.assertGreaterEqual(rc._metrics['backoff'][1], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][2], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][3], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][4], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][5], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][6], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][7], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][8], rc.MIN_REQUEST_RETRY_DELAY()) - self.assertGreaterEqual(rc._metrics['backoff'][9], rc.MIN_REQUEST_RETRY_DELAY()) - - # Ensure delay is never more than the maximum. - self.assertLessEqual(rc._metrics['backoff'][0], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][1], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][2], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][3], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][4], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][5], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][6], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][7], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][8], rc.MAX_REQUEST_RETRY_DELAY()) - self.assertLessEqual(rc._metrics['backoff'][9], rc.MAX_REQUEST_RETRY_DELAY()) - - # Ensure total delay sum is never more than 10s. - self.assertLessEqual(finalBackoff, 10000) - - @mock.patch('requests.post') - def test_post(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - headers = {'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json'} - - mock_post.return_value.text = '{"a": "b"}' - - data = {'some': 'data'} - - mock_post.return_value.status_code = 200 - response = rc.post('the/url', data=data) - mock_post.assert_called_with('the/url', json=data, - headers=headers, timeout=5.0) - - self.assertEqual(response, {'a': 'b'}) - - @mock.patch('requests.post') - def test_post_errors(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_post.return_value.text = '{"statusCode": 999,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_post.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - @mock.patch('requests.post') - def test_post_errors_with_no_message_property(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_post.return_value.text = json.dumps({ - "statusCode": 999, - "errorCode": "code", - "error": "error" - }) - mock_post.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'error') - - @mock.patch('requests.post') - def test_post_errors_with_no_message_or_error_property(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_post.return_value.text = json.dumps({ - "statusCode": 999, - "errorCode": "code" - }) - mock_post.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, '') - - @mock.patch('requests.post') - def test_post_errors_with_message_and_error_property(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_post.return_value.text = json.dumps({ - "statusCode": 999, - "errorCode": "code", - "error": "error", - "message": "message" - }) - mock_post.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - @mock.patch('requests.post') - def test_post_error_with_code_property(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = '{"errorCode": "e0",' \ - '"message": "desc"}' - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'e0') - self.assertEqual(context.exception.message, 'desc') - - @mock.patch('requests.post') - def test_post_error_with_no_error_code(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = '{"message": "desc"}' - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, 'desc') - - @mock.patch('requests.post') - def test_post_error_with_text_response(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = 'there has been a terrible error' - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, - 'there has been a terrible error') - - @mock.patch('requests.post') - def test_post_error_with_no_response_text(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - - for error_status in [400, 500, None]: - mock_post.return_value.status_code = error_status - mock_post.return_value.text = None - - with self.assertRaises(Auth0Error) as context: - rc.post('the-url') - - self.assertEqual(context.exception.status_code, error_status) - self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown') - self.assertEqual(context.exception.message, '') - - @mock.patch('requests.post') - def test_file_post_content_type_is_none(self, mock_post): - rc = RestClient(jwt='a-token', telemetry=False) - headers = {'Authorization': 'Bearer a-token'} - mock_post.return_value.status_code = 200 - mock_post.return_value.text = 'Success' - - data = {'some': 'data'} - files = [mock.Mock()] - - rc.file_post('the-url', data=data, files=files) - - mock_post.assert_called_once_with('the-url', data=data, files=files, headers=headers, timeout=5.0) - - @mock.patch('requests.put') - def test_put(self, mock_put): - rc = RestClient(jwt='a-token', telemetry=False) - headers = {'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json'} - - mock_put.return_value.text = '["a", "b"]' - mock_put.return_value.status_code = 200 - - data = {'some': 'data'} - - response = rc.put(url='the-url', data=data) - mock_put.assert_called_with('the-url', json=data, - headers=headers, timeout=5.0) - - self.assertEqual(response, ['a', 'b']) - - @mock.patch('requests.put') - def test_put_errors(self, mock_put): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_put.return_value.text = '{"statusCode": 999,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_put.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.put(url='the/url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - @mock.patch('requests.patch') - def test_patch(self, mock_patch): - rc = RestClient(jwt='a-token', telemetry=False) - headers = {'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json'} - - mock_patch.return_value.text = '["a", "b"]' - mock_patch.return_value.status_code = 200 - - data = {'some': 'data'} - - response = rc.patch(url='the-url', data=data) - mock_patch.assert_called_with('the-url', json=data, - headers=headers, timeout=5.0) - - self.assertEqual(response, ['a', 'b']) - - @mock.patch('requests.patch') - def test_patch_errors(self, mock_patch): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_patch.return_value.text = '{"statusCode": 999,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_patch.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.patch(url='the/url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - @mock.patch('requests.delete') - def test_delete(self, mock_delete): - rc = RestClient(jwt='a-token', telemetry=False) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - - mock_delete.return_value.text = '["a", "b"]' - mock_delete.return_value.status_code = 200 - - response = rc.delete(url='the-url/ID') - mock_delete.assert_called_with('the-url/ID', headers=headers, params={}, json=None, timeout=5.0) - - self.assertEqual(response, ['a', 'b']) - - @mock.patch('requests.delete') - def test_delete_with_body_and_params(self, mock_delete): - rc = RestClient(jwt='a-token', telemetry=False) - headers = { - 'Authorization': 'Bearer a-token', - 'Content-Type': 'application/json', - } - - mock_delete.return_value.text = '["a", "b"]' - mock_delete.return_value.status_code = 200 - - data = {'some': 'data'} - params = {'A': 'param', 'B': 'param'} - - response = rc.delete(url='the-url/ID', params=params, data=data) - mock_delete.assert_called_with('the-url/ID', headers=headers, params=params, json=data, timeout=5.0) - - self.assertEqual(response, ['a', 'b']) - - @mock.patch('requests.delete') - def test_delete_errors(self, mock_delete): - rc = RestClient(jwt='a-token', telemetry=False) - - mock_delete.return_value.text = '{"statusCode": 999,' \ - ' "errorCode": "code",' \ - ' "message": "message"}' - mock_delete.return_value.status_code = 999 - - with self.assertRaises(Auth0Error) as context: - rc.delete(url='the-url') - - self.assertEqual(context.exception.status_code, 999) - self.assertEqual(context.exception.error_code, 'code') - self.assertEqual(context.exception.message, 'message') - - def test_disabled_telemetry(self): - rc = RestClient(jwt='a-token', telemetry=False) - expected_headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer a-token', - } - - self.assertEqual(rc.base_headers, expected_headers) - - def test_enabled_telemetry(self): - rc = RestClient(jwt='a-token', telemetry=True) - - user_agent = rc.base_headers['User-Agent'] - auth0_client_bytes = base64.b64decode(rc.base_headers['Auth0-Client']) - auth0_client_json = auth0_client_bytes.decode('utf-8') - auth0_client = json.loads(auth0_client_json) - content_type = rc.base_headers['Content-Type'] - - from auth0 import __version__ as auth0_version - python_version = '{}.{}.{}'.format(sys.version_info.major, - sys.version_info.minor, - sys.version_info.micro) - - client_info = { - 'name': 'auth0-python', - 'version': auth0_version, - 'env': { - 'python': python_version - } - } - - self.assertEqual(user_agent, 'Python/{}'.format(python_version)) - self.assertEqual(auth0_client, client_info) - self.assertEqual(content_type, 'application/json') diff --git a/auth0/v3/test/management/test_roles.py b/auth0/v3/test/management/test_roles.py deleted file mode 100644 index bd8caff6..00000000 --- a/auth0/v3/test/management/test_roles.py +++ /dev/null @@ -1,196 +0,0 @@ -import unittest -import mock -from ...management.roles import Roles - - -class TestRoles(unittest.TestCase): - - def test_init_with_optionals(self): - t = Roles(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_list(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.list() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true', - 'name_filter': None - }) - - u.list(page=1, per_page=50, include_totals=False, name_filter='little-role') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false', - 'name_filter': 'little-role' - }) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.create({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/roles', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.get('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id', args[0]) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/roles/an-id' - ) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.update('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_list_users(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.list_users('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/roles/an-id/users', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true', - 'from': None, - 'take': None - }) - - u.list_users(id='an-id', page=1, per_page=50, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/users', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false', - 'from': None, - 'take': None - }) - - u.list_users(id='an-id', from_param=8675309, take=75) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/users', args[0]) - self.assertEqual(kwargs['params'], { - 'from': 8675309, - 'take': 75, - 'per_page': 25, - 'page': 0, - 'include_totals': 'true', - }) - - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_add_users(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.add_users('an-id', ['a', 'b']) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/users', - args[0]) - self.assertEqual(kwargs['data'], {'users': ['a', 'b']}) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_list_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.list_permissions('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/roles/an-id/permissions', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true' - }) - - u.list_permissions(id='an-id', page=1, per_page=50, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/permissions', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false' - }) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_remove_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.remove_permissions('an-id', ['a', 'b']) - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/permissions', - args[0]) - self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']}) - - @mock.patch('auth0.v3.management.roles.RestClient') - def test_add_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Roles(domain='domain', token='jwttoken') - u.add_permissions('an-id', ['a', 'b']) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/roles/an-id/permissions', - args[0]) - self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']}) diff --git a/auth0/v3/test/management/test_rules.py b/auth0/v3/test/management/test_rules.py deleted file mode 100644 index d8955da8..00000000 --- a/auth0/v3/test/management/test_rules.py +++ /dev/null @@ -1,117 +0,0 @@ -import unittest -import mock -from ...management.rules import Rules - - -class TestRules(unittest.TestCase): - - def test_init_with_optionals(self): - t = Rules(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.rules.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Rules(domain='domain', token='jwttoken') - - # with default params - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'enabled': 'true', - 'stage': 'login_success', - 'page': None, - 'per_page': None, - 'include_totals': 'false'}) - - # with stage and fields params - c.all(stage='stage', enabled=False, fields=['a', 'b'], - include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false', - 'enabled': 'false', - 'stage': 'stage', - 'page': None, - 'per_page': None, - 'include_totals': 'false'}) - - # with pagination params - c.all(page=3, per_page=27, include_totals=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true', - 'enabled': 'true', - 'stage': 'login_success', - 'page': 3, - 'per_page': 27, - 'include_totals': 'true'}) - - @mock.patch('auth0.v3.management.rules.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Rules(domain='domain', token='jwttoken') - c.create({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/rules', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.rules.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Rules(domain='domain', token='jwttoken') - c.get('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - c.get('an-id', fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - @mock.patch('auth0.v3.management.rules.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Rules(domain='domain', token='jwttoken') - c.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/rules/an-id' - ) - - @mock.patch('auth0.v3.management.rules.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - c = Rules(domain='domain', token='jwttoken') - c.update('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/rules/an-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) diff --git a/auth0/v3/test/management/test_rules_configs.py b/auth0/v3/test/management/test_rules_configs.py deleted file mode 100644 index f4ee868a..00000000 --- a/auth0/v3/test/management/test_rules_configs.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -import mock -from ...management.rules_configs import RulesConfigs - - -class TestRulesConfigs(unittest.TestCase): - - def test_init_with_optionals(self): - t = RulesConfigs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.rules_configs.RestClient') - def test_all(self, mock_rc): - mock_instance = mock_rc.return_value - - c = RulesConfigs(domain='domain', token='jwttoken') - - c.all() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/rules-configs', args[0]) - - @mock.patch('auth0.v3.management.rules_configs.RestClient') - def test_unset(self, mock_rc): - mock_instance = mock_rc.return_value - - c = RulesConfigs(domain='domain', token='jwttoken') - c.unset('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/rules-configs/an-id' - ) - - @mock.patch('auth0.v3.management.rules_configs.RestClient') - def test_set(self, mock_rc): - mock_instance = mock_rc.return_value - - g = RulesConfigs(domain='domain', token='jwttoken') - g.set('key', 'MY_RULES_CONFIG_VALUES') - - args, kwargs = mock_instance.put.call_args - self.assertEqual('https://domain/api/v2/rules-configs/key', args[0]) - - - - diff --git a/auth0/v3/test/management/test_stats.py b/auth0/v3/test/management/test_stats.py deleted file mode 100644 index 4872759c..00000000 --- a/auth0/v3/test/management/test_stats.py +++ /dev/null @@ -1,42 +0,0 @@ -import unittest -import mock -from ...management.stats import Stats - - -class TestStats(unittest.TestCase): - - def test_init_with_optionals(self): - t = Stats(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.stats.RestClient') - def test_active_users(self, mock_rc): - mock_instance = mock_rc.return_value - - s = Stats(domain='domain', token='jwttoken') - s.active_users() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/stats/active-users', - ) - - @mock.patch('auth0.v3.management.stats.RestClient') - def test_daily_stats(self, mock_rc): - mock_instance = mock_rc.return_value - - s = Stats(domain='domain', token='jwttoken') - s.daily_stats() - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/stats/daily', - params={'from': None, 'to': None}, - ) - - s.daily_stats(from_date='12341212', to_date='56785656') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/stats/daily', - params={'from': '12341212', 'to': '56785656'}, - ) diff --git a/auth0/v3/test/management/test_tenants.py b/auth0/v3/test/management/test_tenants.py deleted file mode 100644 index e0c9a864..00000000 --- a/auth0/v3/test/management/test_tenants.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -import mock -from ...management.tenants import Tenants - - -class TestTenants(unittest.TestCase): - - def test_init_with_optionals(self): - t = Tenants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.tenants.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.get.return_value = {} - - t = Tenants(domain='domain', token='jwttoken') - t.get() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/tenants/settings', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - t.get(fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/tenants/settings', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - t.get(fields=['a', 'b'], include_fields=True) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/tenants/settings', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'true'}) - - @mock.patch('auth0.v3.management.tenants.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - mock_instance.patch.return_value = {} - - t = Tenants(domain='domain', token='jwttoken') - t.update({'a': 'b', 'c': 'd'}) - - mock_instance.patch.assert_called_with( - 'https://domain/api/v2/tenants/settings', - data={'a': 'b', 'c': 'd'} - ) diff --git a/auth0/v3/test/management/test_tickets.py b/auth0/v3/test/management/test_tickets.py deleted file mode 100644 index eafbe626..00000000 --- a/auth0/v3/test/management/test_tickets.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest -import mock -from ...management.tickets import Tickets - - -class TestTickets(unittest.TestCase): - - def test_init_with_optionals(self): - t = Tickets(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.tickets.RestClient') - def test_email(self, mock_rc): - mock_instance = mock_rc.return_value - - t = Tickets(domain='domain', token='jwttoken') - t.create_email_verification({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/tickets/email-verification', - data={'a': 'b', 'c': 'd'} - ) - - @mock.patch('auth0.v3.management.tickets.RestClient') - def test_pswd(self, mock_rc): - mock_instance = mock_rc.return_value - - t = Tickets(domain='domain', token='jwttoken') - t.create_pswd_change({'a': 'b', 'c': 'd'}) - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/tickets/password-change', - data={'a': 'b', 'c': 'd'} - ) diff --git a/auth0/v3/test/management/test_user_blocks.py b/auth0/v3/test/management/test_user_blocks.py deleted file mode 100644 index 8d81a8bd..00000000 --- a/auth0/v3/test/management/test_user_blocks.py +++ /dev/null @@ -1,62 +0,0 @@ -import unittest -import mock -from ...management.user_blocks import UserBlocks - - -class TestUserBlocks(unittest.TestCase): - - def test_init_with_optionals(self): - t = UserBlocks(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.user_blocks.RestClient') - def test_get_by_identifier(self, mock_rc): - mock_instance = mock_rc.return_value - - u = UserBlocks(domain='domain', token='jwttoken') - - u.get_by_identifier('some_identifier') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/user-blocks', - params={'identifier': 'some_identifier'} - ) - - @mock.patch('auth0.v3.management.user_blocks.RestClient') - def test_unblock_by_identifier(self, mock_rc): - mock_instance = mock_rc.return_value - - u = UserBlocks(domain='domain', token='jwttoken') - - u.unblock_by_identifier('test@test.com') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/user-blocks', - params={'identifier': 'test@test.com'} - ) - - @mock.patch('auth0.v3.management.user_blocks.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - u = UserBlocks(domain='domain', token='jwttoken') - - u.get('auth0|584ad3c228be27504a2c80d5') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/user-blocks/auth0|584ad3c228be27504a2c80d5' - ) - - @mock.patch('auth0.v3.management.user_blocks.RestClient') - def test_unblock(self, mock_rc): - mock_instance = mock_rc.return_value - - u = UserBlocks(domain='domain', token='jwttoken') - - u.unblock('auth0|584ad3c228be27504a2c80d5') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/user-blocks/auth0|584ad3c228be27504a2c80d5' - ) diff --git a/auth0/v3/test/management/test_users.py b/auth0/v3/test/management/test_users.py deleted file mode 100644 index 5d566aba..00000000 --- a/auth0/v3/test/management/test_users.py +++ /dev/null @@ -1,342 +0,0 @@ -import unittest -import mock -from ...management.users import Users - - -class TestUsers(unittest.TestCase): - - def test_init_with_optionals(self): - t = Users(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_list(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.list() - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true', - 'sort': None, - 'connection': None, - 'fields': None, - 'include_fields': 'true', - 'q': None, - 'search_engine': None - }) - - u.list(page=1, per_page=50, sort='s', connection='con', q='q', - search_engine='se', include_totals=False, fields=['a', 'b'], - include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false', - 'sort': 's', - 'connection': 'con', - 'fields': 'a,b', - 'include_fields': 'false', - 'q': 'q', - 'search_engine': 'se' - }) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_create(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.create({'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/users', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_delete_all_users(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.delete_all_users() - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/users' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_get(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.get('an-id') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': None, - 'include_fields': 'true'}) - - u.get('an-id', fields=['a', 'b'], include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users/an-id', args[0]) - self.assertEqual(kwargs['params'], {'fields': 'a,b', - 'include_fields': 'false'}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_delete(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.delete('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/users/an-id' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_update(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.update('an-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.patch.call_args - - self.assertEqual('https://domain/api/v2/users/an-id', args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_list_organizations(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.list_organizations('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/users/an-id/organizations', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true' - }) - - u.list_organizations(id='an-id', page=1, per_page=50, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/organizations', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false' - }) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_list_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.list_roles('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/users/an-id/roles', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true' - }) - - u.list_roles(id='an-id', page=1, per_page=50, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/roles', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false' - }) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_remove_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.remove_roles('an-id', ['a', 'b']) - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/roles', - args[0]) - self.assertEqual(kwargs['data'], {'roles': ['a', 'b']}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_add_roles(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.add_roles('an-id', ['a', 'b']) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/roles', - args[0]) - self.assertEqual(kwargs['data'], {'roles': ['a', 'b']}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_list_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.list_permissions('an-id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/users/an-id/permissions', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 25, - 'page': 0, - 'include_totals': 'true' - }) - - u.list_permissions(id='an-id', page=1, per_page=50, include_totals=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/permissions', args[0]) - self.assertEqual(kwargs['params'], { - 'per_page': 50, - 'page': 1, - 'include_totals': 'false' - }) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_remove_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.remove_permissions('an-id', ['a', 'b']) - - args, kwargs = mock_instance.delete.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/permissions', - args[0]) - self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_add_permissions(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.add_permissions('an-id', ['a', 'b']) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/users/an-id/permissions', - args[0]) - self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_delete_multifactor(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.delete_multifactor('an-id', 'provider') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/users/an-id/multifactor/provider' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_delete_authenticators(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.delete_authenticators('an-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/users/an-id/authenticators' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_unlink_user_account(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.unlink_user_account('an-id', 'provider', 'user-id') - - mock_instance.delete.assert_called_with( - 'https://domain/api/v2/users/an-id/identities/provider/user-id' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_link_user_account(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.link_user_account('user-id', {'a': 'b', 'c': 'd'}) - - args, kwargs = mock_instance.post.call_args - - self.assertEqual('https://domain/api/v2/users/user-id/identities', - args[0]) - self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'}) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_regenerate_recovery_code(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.regenerate_recovery_code('user-id') - - mock_instance.post.assert_called_with( - 'https://domain/api/v2/users/user-id/recovery-code-regeneration' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_get_guardian_enrollments(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.get_guardian_enrollments('user-id') - - mock_instance.get.assert_called_with( - 'https://domain/api/v2/users/user-id/enrollments' - ) - - @mock.patch('auth0.v3.management.users.RestClient') - def test_get_log_events(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.get_log_events('used_id') - - args, kwargs = mock_instance.get.call_args - self.assertEqual('https://domain/api/v2/users/used_id/logs', args[0]) - self.assertEqual(kwargs['params']['page'], 0) - self.assertEqual(kwargs['params']['per_page'], 50) - self.assertIsNone(kwargs['params']['sort']) - self.assertEqual(kwargs['params']['include_totals'], 'false') - - @mock.patch('auth0.v3.management.users.RestClient') - def test_invalidate_remembered_browsers(self, mock_rc): - mock_instance = mock_rc.return_value - - u = Users(domain='domain', token='jwttoken') - u.invalidate_remembered_browsers('user-id') - - args, kwargs = mock_instance.post.call_args - self.assertEqual('https://domain/api/v2/users/user-id/multifactor/actions/invalidate-remember-browser', args[0]) diff --git a/auth0/v3/test/management/test_users_by_email.py b/auth0/v3/test/management/test_users_by_email.py deleted file mode 100644 index 8ab8695a..00000000 --- a/auth0/v3/test/management/test_users_by_email.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest -import mock -from ...management.users_by_email import UsersByEmail - - -class TestUsersByEmail(unittest.TestCase): - - def test_init_with_optionals(self): - t = UsersByEmail(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2)) - self.assertEqual(t.client.options.timeout, (10, 2)) - telemetry_header = t.client.base_headers.get('Auth0-Client', None) - self.assertEqual(telemetry_header, None) - - @mock.patch('auth0.v3.management.users_by_email.RestClient') - def test_search_users_by_email(self, mock_rc): - mock_instance = mock_rc.return_value - - u = UsersByEmail(domain='domain', token='jwttoken') - u.search_users_by_email('A@B.com') - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users-by-email', args[0]) - self.assertEqual(kwargs['params'], { - 'email': 'A@B.com', - 'fields': None, - 'include_fields': 'true' - }) - - u.search_users_by_email(email='a@b.com', - fields=['a', 'b'], - include_fields=False) - - args, kwargs = mock_instance.get.call_args - - self.assertEqual('https://domain/api/v2/users-by-email', args[0]) - self.assertEqual(kwargs['params'], { - 'email': 'a@b.com', - 'fields': 'a,b', - 'include_fields': 'false' - }) diff --git a/docs/Makefile b/docs/Makefile index fae9a307..f370b898 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -W --keep-going -n -a +SPHINXOPTS ?= --keep-going -n -a SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index 0afaf710..46a138e1 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -8,7 +8,7 @@ if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source -set BUILDDIR=../auth0/v3 +set BUILDDIR=../auth0 if "%1" == "" goto help diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 31a6de84..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx==4.1.2 -sphinx-rtd-theme==0.5.2 \ No newline at end of file diff --git a/docs/source/v3.authentication.rst b/docs/source/authentication.rst similarity index 59% rename from docs/source/v3.authentication.rst rename to docs/source/authentication.rst index 303858e3..f74fb157 100644 --- a/docs/source/v3.authentication.rst +++ b/docs/source/authentication.rst @@ -1,18 +1,10 @@ authentication package ========================= -authentication.authorize\_client module ------------------------------------------- - -.. automodule:: auth0.v3.authentication.authorize_client - :members: - :undoc-members: - :show-inheritance: - authentication.base module ----------------------------- -.. automodule:: auth0.v3.authentication.base +.. automodule:: auth0.authentication.base :members: :undoc-members: :show-inheritance: @@ -20,7 +12,7 @@ authentication.base module authentication.database module --------------------------------- -.. automodule:: auth0.v3.authentication.database +.. automodule:: auth0.authentication.database :members: :undoc-members: :show-inheritance: @@ -28,7 +20,7 @@ authentication.database module authentication.delegated module ---------------------------------- -.. automodule:: auth0.v3.authentication.delegated +.. automodule:: auth0.authentication.delegated :members: :undoc-members: :show-inheritance: @@ -36,7 +28,7 @@ authentication.delegated module authentication.enterprise module ----------------------------------- -.. automodule:: auth0.v3.authentication.enterprise +.. automodule:: auth0.authentication.enterprise :members: :undoc-members: :show-inheritance: @@ -44,15 +36,7 @@ authentication.enterprise module authentication.get\_token module ----------------------------------- -.. automodule:: auth0.v3.authentication.get_token - :members: - :undoc-members: - :show-inheritance: - -authentication.logout module -------------------------------- - -.. automodule:: auth0.v3.authentication.logout +.. automodule:: auth0.authentication.get_token :members: :undoc-members: :show-inheritance: @@ -60,7 +44,7 @@ authentication.logout module authentication.passwordless module ------------------------------------- -.. automodule:: auth0.v3.authentication.passwordless +.. automodule:: auth0.authentication.passwordless :members: :undoc-members: :show-inheritance: @@ -68,7 +52,7 @@ authentication.passwordless module authentication.revoke\_token module -------------------------------------- -.. automodule:: auth0.v3.authentication.revoke_token +.. automodule:: auth0.authentication.revoke_token :members: :undoc-members: :show-inheritance: @@ -76,7 +60,7 @@ authentication.revoke\_token module authentication.social module ------------------------------- -.. automodule:: auth0.v3.authentication.social +.. automodule:: auth0.authentication.social :members: :undoc-members: :show-inheritance: @@ -84,7 +68,7 @@ authentication.social module authentication.token\_verifier module ---------------------------------------- -.. automodule:: auth0.v3.authentication.token_verifier +.. automodule:: auth0.authentication.token_verifier :members: :undoc-members: :show-inheritance: @@ -92,7 +76,7 @@ authentication.token\_verifier module authentication.users module ------------------------------ -.. automodule:: auth0.v3.authentication.users +.. automodule:: auth0.authentication.users :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index cb04f7d2..c62037d4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,19 +11,22 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os -import io import re import sys -sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, os.path.abspath("../..")) + # -- helper function to read a file without importing it def read(*names, **kwargs): - with io.open( - os.path.join(os.path.dirname(__file__)[:-7], *names), encoding=kwargs.get("encoding", "utf8") + with open( + os.path.join(os.path.dirname(__file__)[:-7], *names), + encoding=kwargs.get("encoding", "utf8"), ) as fp: return fp.read() + # -- helper function to get the __version__ from a file def find_version(*file_paths): version_file = read(*file_paths) @@ -32,14 +35,15 @@ def find_version(*file_paths): return version_match.group(1) raise RuntimeError("Unable to find version string.") + # -- regenerate autodoc definitions -# sphinx-apidoc -o ./source ../auth0/v3/ +# sphinx-apidoc -o ./source ../auth0/ # -- Project information ----------------------------------------------------- -project = 'auth0-python' -copyright = '2021, Auth0' -author = 'Auth0' +project = "auth0-python" +copyright = "2021, Auth0" +author = "Auth0" # The full version, including alpha/beta/rc tags release = find_version("..", "auth0", "__init__.py") @@ -51,13 +55,15 @@ def find_version(*file_paths): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx_mdinclude", + "sphinx_autodoc_typehints", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -68,13 +74,13 @@ def find_version(*file_paths): # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = [".rst", ".md"] # The master toctree document. -master_doc = 'index' +master_doc = "index" # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- @@ -82,9 +88,17 @@ def find_version(*file_paths): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] \ No newline at end of file +html_static_path = [] + +# Sphinx somehow can't find this one +nitpick_ignore = [ + ("py:class", "cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey"), + ("py:class", "RSAPublicKey"), + ("py:data", "typing.Any"), + ("py:data", "typing.ClassVar"), +] diff --git a/docs/source/v3.exceptions.rst b/docs/source/exceptions.rst similarity index 78% rename from docs/source/v3.exceptions.rst rename to docs/source/exceptions.rst index 261a1f44..e4f725c9 100644 --- a/docs/source/v3.exceptions.rst +++ b/docs/source/exceptions.rst @@ -4,7 +4,7 @@ exceptions module Module contents --------------- -.. automodule:: auth0.v3.exceptions +.. automodule:: auth0.exceptions :members: :undoc-members: :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst index 7c449e4c..b2c0deac 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,25 +1,17 @@ Auth0-Python documentation ======================================== -.. include:: ../../README.rst +.. mdInclude:: ../../README.md .. toctree:: :hidden: :caption: Learn the basics - + readme_content .. toctree:: :hidden: :caption: API Documentation - v3.authentication - v3.management - v3.exceptions - -================== -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + authentication + management + exceptions diff --git a/docs/source/v3.management.rst b/docs/source/management.rst similarity index 66% rename from docs/source/v3.management.rst rename to docs/source/management.rst index 8bc17b22..8a58a8ee 100644 --- a/docs/source/v3.management.rst +++ b/docs/source/management.rst @@ -4,7 +4,7 @@ management package management.auth0 module -------------------------- -.. automodule:: auth0.v3.management.auth0 +.. automodule:: auth0.management.auth0 :members: :undoc-members: :show-inheritance: @@ -12,7 +12,15 @@ management.auth0 module management.blacklists module ------------------------------- -.. automodule:: auth0.v3.management.blacklists +.. automodule:: auth0.management.blacklists + :members: + :undoc-members: + :show-inheritance: + +management.branding module +------------------------------- + +.. automodule:: auth0.management.branding :members: :undoc-members: :show-inheritance: @@ -20,7 +28,7 @@ management.blacklists module management.client\_grants module ----------------------------------- -.. automodule:: auth0.v3.management.client_grants +.. automodule:: auth0.management.client_grants :members: :undoc-members: :show-inheritance: @@ -28,7 +36,7 @@ management.client\_grants module management.clients module ---------------------------- -.. automodule:: auth0.v3.management.clients +.. automodule:: auth0.management.clients :members: :undoc-members: :show-inheritance: @@ -36,7 +44,7 @@ management.clients module management.connections module -------------------------------- -.. automodule:: auth0.v3.management.connections +.. automodule:: auth0.management.connections :members: :undoc-members: :show-inheritance: @@ -44,7 +52,7 @@ management.connections module management.custom\_domains module ------------------------------------ -.. automodule:: auth0.v3.management.custom_domains +.. automodule:: auth0.management.custom_domains :members: :undoc-members: :show-inheritance: @@ -52,7 +60,7 @@ management.custom\_domains module management.device\_credentials module ---------------------------------------- -.. automodule:: auth0.v3.management.device_credentials +.. automodule:: auth0.management.device_credentials :members: :undoc-members: :show-inheritance: @@ -60,7 +68,7 @@ management.device\_credentials module management.email\_templates module ------------------------------------- -.. automodule:: auth0.v3.management.email_templates +.. automodule:: auth0.management.email_templates :members: :undoc-members: :show-inheritance: @@ -68,7 +76,7 @@ management.email\_templates module management.emails module --------------------------- -.. automodule:: auth0.v3.management.emails +.. automodule:: auth0.management.emails :members: :undoc-members: :show-inheritance: @@ -76,7 +84,7 @@ management.emails module management.grants module --------------------------- -.. automodule:: auth0.v3.management.grants +.. automodule:: auth0.management.grants :members: :undoc-members: :show-inheritance: @@ -84,7 +92,7 @@ management.grants module management.guardian module ----------------------------- -.. automodule:: auth0.v3.management.guardian +.. automodule:: auth0.management.guardian :members: :undoc-members: :show-inheritance: @@ -92,7 +100,7 @@ management.guardian module management.hooks module -------------------------- -.. automodule:: auth0.v3.management.hooks +.. automodule:: auth0.management.hooks :members: :undoc-members: :show-inheritance: @@ -100,7 +108,7 @@ management.hooks module management.jobs module ------------------------- -.. automodule:: auth0.v3.management.jobs +.. automodule:: auth0.management.jobs :members: :undoc-members: :show-inheritance: @@ -108,7 +116,7 @@ management.jobs module management.log\_streams module --------------------------------- -.. automodule:: auth0.v3.management.log_streams +.. automodule:: auth0.management.log_streams :members: :undoc-members: :show-inheritance: @@ -116,39 +124,39 @@ management.log\_streams module management.logs module ------------------------- -.. automodule:: auth0.v3.management.logs +.. automodule:: auth0.management.logs :members: :undoc-members: :show-inheritance: -management.organizations module ----------------------------------- +management.network\_acls module +----------------------------------------- -.. automodule:: auth0.v3.management.organizations +.. automodule:: auth0.management.network_acls :members: :undoc-members: :show-inheritance: -management.prompts module +management.organizations module ---------------------------------- -.. automodule:: auth0.v3.management.prompts +.. automodule:: auth0.management.organizations :members: :undoc-members: :show-inheritance: -management.resource\_servers module --------------------------------------- +management.prompts module +---------------------------------- -.. automodule:: auth0.v3.management.resource_servers +.. automodule:: auth0.management.prompts :members: :undoc-members: :show-inheritance: -management.rest module -------------------------- +management.resource\_servers module +-------------------------------------- -.. automodule:: auth0.v3.management.rest +.. automodule:: auth0.management.resource_servers :members: :undoc-members: :show-inheritance: @@ -156,7 +164,7 @@ management.rest module management.roles module -------------------------- -.. automodule:: auth0.v3.management.roles +.. automodule:: auth0.management.roles :members: :undoc-members: :show-inheritance: @@ -164,7 +172,7 @@ management.roles module management.rules\_configs module ----------------------------------- -.. automodule:: auth0.v3.management.rules_configs +.. automodule:: auth0.management.rules_configs :members: :undoc-members: :show-inheritance: @@ -172,15 +180,23 @@ management.rules\_configs module management.rules module -------------------------- -.. automodule:: auth0.v3.management.rules +.. automodule:: auth0.management.rules :members: :undoc-members: :show-inheritance: +management.self\_service\_profiles module +----------------------------------------- + +.. automodule:: auth0.management.self_service_profiles + :members: + :undoc-members: + :show-inheritance: + management.stats module -------------------------- -.. automodule:: auth0.v3.management.stats +.. automodule:: auth0.management.stats :members: :undoc-members: :show-inheritance: @@ -188,7 +204,7 @@ management.stats module management.tenants module ---------------------------- -.. automodule:: auth0.v3.management.tenants +.. automodule:: auth0.management.tenants :members: :undoc-members: :show-inheritance: @@ -196,7 +212,7 @@ management.tenants module management.tickets module ---------------------------- -.. automodule:: auth0.v3.management.tickets +.. automodule:: auth0.management.tickets :members: :undoc-members: :show-inheritance: @@ -204,7 +220,7 @@ management.tickets module management.user\_blocks module --------------------------------- -.. automodule:: auth0.v3.management.user_blocks +.. automodule:: auth0.management.user_blocks :members: :undoc-members: :show-inheritance: @@ -212,7 +228,7 @@ management.user\_blocks module management.users\_by\_email module ------------------------------------- -.. automodule:: auth0.v3.management.users_by_email +.. automodule:: auth0.management.users_by_email :members: :undoc-members: :show-inheritance: @@ -220,7 +236,7 @@ management.users\_by\_email module management.users module -------------------------- -.. automodule:: auth0.v3.management.users +.. automodule:: auth0.management.users :members: :undoc-members: :show-inheritance: diff --git a/docs/source/readme_content.rst b/docs/source/readme_content.rst index 38ba8043..3bd447c4 100644 --- a/docs/source/readme_content.rst +++ b/docs/source/readme_content.rst @@ -1 +1 @@ -.. include:: ../../README.rst \ No newline at end of file +.. mdinclude:: ../../README.md diff --git a/examples/flask-webapp/README.md b/examples/flask-webapp/README.md index e82a9e19..3b836ed4 100644 --- a/examples/flask-webapp/README.md +++ b/examples/flask-webapp/README.md @@ -1,3 +1,3 @@ # Deprecation Notice -These samples have been deprecated. Please see the new Python API samples [here](https://github.com/auth0-samples/auth0-python-web-app). \ No newline at end of file +These samples have been deprecated. Please see the new Python API samples [here](https://github.com/auth0-samples/auth0-python-web-app). diff --git a/examples/webapi2/README.md b/examples/webapi2/README.md index 75dd562a..70581905 100644 --- a/examples/webapi2/README.md +++ b/examples/webapi2/README.md @@ -1,3 +1,3 @@ # Deprecation Notice -These samples have been deprecated. Please see the new Python API samples [here](https://github.com/auth0-samples/auth0-python-api-samples). \ No newline at end of file +These samples have been deprecated. Please see the new Python API samples [here](https://github.com/auth0-samples/auth0-python-api-samples). diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..cdfec98a --- /dev/null +++ b/mypy.ini @@ -0,0 +1,15 @@ +[mypy] +python_version = 3.7 + +[mypy-auth0.test.*,auth0.test_async.*] +ignore_errors = True + +[mypy-auth0.management.*] +ignore_errors = False +disable_error_code=var-annotated + +[mypy-auth0.rest_async] +disable_error_code=override + +[mypy-auth0.authentication.async_token_verifier] +disable_error_code=override, misc, attr-defined diff --git a/opslevel.yml b/opslevel.yml new file mode 100644 index 00000000..009a5ec0 --- /dev/null +++ b/opslevel.yml @@ -0,0 +1,6 @@ +--- +version: 1 +repository: + owner: dx_sdks + tier: + tags: diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..381bef4f --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1420 @@ +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.11" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"}, + {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"}, + {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"}, + {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"}, + {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"}, + {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"}, + {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"}, + {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"}, + {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"}, + {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"}, + {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"}, + {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"}, + {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"}, + {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.12.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aioresponses" +version = "0.7.8" +description = "Mock out requests made by ClientSession from aiohttp package" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94"}, + {file = "aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11"}, +] + +[package.dependencies] +aiohttp = ">=3.3.0,<4.0.0" +packaging = ">=22.0" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "argcomplete" +version = "3.6.2" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, + {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "mock" +version = "5.2.0" +description = "Rolling backport of unittest.mock for all Pythons" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, + {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, +] + +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pipx" +version = "1.7.1" +description = "Install and Run Python Applications in Isolated Environments" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pipx-1.7.1-py3-none-any.whl", hash = "sha256:3933c43bb344e649cb28e10d357e0967ce8572f1c19caf90cf39ae95c2a0afaf"}, + {file = "pipx-1.7.1.tar.gz", hash = "sha256:762de134e16a462be92645166d225ecef446afaef534917f5f70008d63584360"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4" +colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} +packaging = ">=20" +platformdirs = ">=2.1" +tomli = {version = "*", markers = "python_version < \"3.11\""} +userpath = ">=1.6,<1.9 || >1.9" + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pyjwt" +version = "2.9.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-aiohttp" +version = "1.0.5" +description = "Pytest plugin for aiohttp support" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, + {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, +] + +[package.dependencies] +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "responses" +version = "0.25.8" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c"}, + {file = "responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "userpath" +version = "1.9.2" +description = "Cross-platform tool for adding locations to the user PATH" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d"}, + {file = "userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815"}, +] + +[package.dependencies] +click = "*" + +[[package]] +name = "yarl" +version = "1.15.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8"}, + {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172"}, + {file = "yarl-1.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f"}, + {file = "yarl-1.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed"}, + {file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec"}, + {file = "yarl-1.15.2-cp310-cp310-win32.whl", hash = "sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75"}, + {file = "yarl-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e"}, + {file = "yarl-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c"}, + {file = "yarl-1.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931"}, + {file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5"}, + {file = "yarl-1.15.2-cp311-cp311-win32.whl", hash = "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d"}, + {file = "yarl-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e"}, + {file = "yarl-1.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f"}, + {file = "yarl-1.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715"}, + {file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b"}, + {file = "yarl-1.15.2-cp312-cp312-win32.whl", hash = "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8"}, + {file = "yarl-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33"}, + {file = "yarl-1.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b"}, + {file = "yarl-1.15.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644"}, + {file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe"}, + {file = "yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9"}, + {file = "yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b"}, + {file = "yarl-1.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d"}, + {file = "yarl-1.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d"}, + {file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04"}, + {file = "yarl-1.15.2-cp38-cp38-win32.whl", hash = "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea"}, + {file = "yarl-1.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627"}, + {file = "yarl-1.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb"}, + {file = "yarl-1.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036"}, + {file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7"}, + {file = "yarl-1.15.2-cp39-cp39-win32.whl", hash = "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d"}, + {file = "yarl-1.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810"}, + {file = "yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a"}, + {file = "yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.8" +content-hash = "f2b4849cb247bdefbbc9b155d28c5e1bcf9b4ca3c2c92defd30b21206018fa3f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..83c499fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" + +[tool.poetry] +name = "auth0-python" +version = "0.0.0" # This is replaced by dynamic versioning +description = "" +authors = ["Auth0 "] +license = "MIT" +repository = "https://github.com/auth0/auth0-python" +homepage = "https://auth0.com" +readme = "README.md" +packages = [{ include = "auth0" }] + +[tool.poetry-dynamic-versioning] +strict = true +enable = true +vcs = "git" +style = "semver" +format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}{% endif %}" +pattern = "default-unprefixed" + +[tool.poetry-dynamic-versioning.substitution] +files = ["*/__init__.py"] +folders = [{ path = "auth0" }] + +[tool.poetry.dependencies] +python = ">=3.8" +aiohttp = ">=3.10.11" +cryptography = ">=43.0.1" # pyjwt has a weak dependency on cryptography +pyjwt = ">=2.8.0" +requests = ">=2.32.3" +urllib3 = ">=2.2.3" # requests has a weak dependency on urllib3 + +[tool.poetry.group.dev.dependencies] +aioresponses = "^0.7.8" +mock = "^5.1.0" +pipx = "^1.7.1" +pytest = "^7.4.0" +pytest-aiohttp = "^1.0.4" +pytest-asyncio = ">=0.21.1,<0.24.0" +pytest-cov = "^4.1.0" +responses = ">=0.23.3,<0.26.0" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e1ae8bd3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,41 @@ +aiohttp==3.8.6 ; python_version >= "3.7" and python_version < "4.0" +aioresponses==0.7.4 ; python_version >= "3.7" and python_version < "4.0" +aiosignal==1.4.0 ; python_version >= "3.7" and python_version < "4.0" +argcomplete==3.5.3 ; python_version >= "3.7" and python_version < "4.0" +async-timeout==4.0.3 ; python_version >= "3.7" and python_version < "4.0" +asynctest==0.13.0 ; python_version >= "3.7" and python_version < "3.8" +attrs==23.1.0 ; python_version >= "3.7" and python_version < "4.0" +certifi==2025.8.3 ; python_version >= "3.7" and python_version < "4.0" +cffi==1.17.1 ; python_version >= "3.7" and python_version < "4.0" +charset-normalizer==3.4.3 ; python_version >= "3.7" and python_version < "4.0" +click==8.1.8 ; python_version >= "3.7" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" +coverage[toml]==7.9.2 ; python_version >= "3.7" and python_version < "4.0" +cryptography==44.0.1 ; python_version >= "3.7" and python_version < "4.0" +exceptiongroup==1.3.0 ; python_version >= "3.7" and python_version < "3.11" +frozenlist==1.7.0 ; python_version >= "3.7" and python_version < "4.0" +idna==3.10 ; python_version >= "3.7" and python_version < "4.0" +importlib-metadata==6.7.0 ; python_version >= "3.7" and python_version < "3.8" +iniconfig==2.1.0 ; python_version >= "3.7" and python_version < "4.0" +mock==5.2.0 ; python_version >= "3.7" and python_version < "4.0" +multidict==6.0.4 ; python_version >= "3.7" and python_version < "4.0" +packaging==23.2 ; python_version >= "3.7" and python_version < "4.0" +pipx==1.2.0 ; python_version >= "3.7" and python_version < "4.0" +pluggy==1.6.0 ; python_version >= "3.7" and python_version < "4.0" +pycparser==2.23 ; python_version >= "3.7" and python_version < "4.0" +pyjwt==2.9.0 ; python_version >= "3.7" and python_version < "4.0" +pyopenssl==25.2.0 ; python_version >= "3.7" and python_version < "4.0" +pytest-aiohttp==1.0.5 ; python_version >= "3.7" and python_version < "4.0" +pytest-asyncio==0.23.8 ; python_version >= "3.7" and python_version < "4.0" +pytest-cov==4.1.0 ; python_version >= "3.7" and python_version < "4.0" +pytest==7.4.0 ; python_version >= "3.7" and python_version < "4.0" +pyyaml==6.0.2 ; python_version >= "3.7" and python_version < "4.0" +requests==2.32.4 ; python_version >= "3.7" and python_version < "4.0" +responses==0.23.3 ; python_version >= "3.7" and python_version < "4.0" +tomli==2.2.1 ; python_version >= "3.7" and python_full_version <= "3.11.0a6" +types-pyyaml==6.0.12.20250915 ; python_version >= "3.7" and python_version < "4.0" +typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "3.8" +urllib3==2.6.0 ; python_version >= "3.7" and python_version < "4.0" +userpath==1.9.2 ; python_version >= "3.7" and python_version < "4.0" +yarl==1.20.0 ; python_version >= "3.7" and python_version < "4.0" +zipp==3.19.1 ; python_version >= "3.7" and python_version < "3.8" diff --git a/setup.py b/setup.py index 18a3b203..60684932 100644 --- a/setup.py +++ b/setup.py @@ -1,49 +1,3 @@ -import io -import os -import re -from setuptools import setup, find_packages +from setuptools import setup - -def find_version(): - file_dir = os.path.dirname(__file__) - with io.open(os.path.join(file_dir, 'auth0', '__init__.py')) as f: - version = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', f.read()) - if version: - return version.group(1) - else: - raise RuntimeError("Unable to find version string.") - - -with io.open('README.rst', encoding='utf-8') as f: - long_description = f.read() - - -setup( - name='auth0-python', - version=find_version(), - description='Auth0 Python SDK', - long_description=long_description, - author='Auth0', - author_email='support@auth0.com', - license='MIT', - packages=find_packages(), - install_requires=['requests>=2.14.0', 'pyjwt[crypto]>=1.7.1'], - extras_require={'test': ['mock>=1.3.0', 'pre-commit']}, - python_requires='>=2.7, !=3.0.*, !=3.1.*', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - ], - url='https://github.com/auth0/auth0-python', -) +setup()