diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2950dcf7..efc14b01 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @samwelkanda @jobala @ddyett @MichaelMainer @shemogumbe +@microsoftgraph/msgraph-devx-python-write diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d68d970b..36a6b4a1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,13 +1,12 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: samwelkanda - --- --- + name: Bug report about: Create a report to help us improve title: '' @@ -16,15 +15,20 @@ assignees: '' --- +**Environment** + +- Python Version +- msgraph-core version: +- OS: + +**Stack trace (if available)** +Screenshot or `formatted` copy and paste of your stack trace. + **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error **Expected behavior** A clear and concise description of what you expected to happen. @@ -33,4 +37,4 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Additional context** -Add any other context about the problem here. \ No newline at end of file +Add any other context about the problem here. diff --git a/.github/RELEASE-TEMPLATE.md b/.github/RELEASE-TEMPLATE.md deleted file mode 100644 index 7d76e2d1..00000000 --- a/.github/RELEASE-TEMPLATE.md +++ /dev/null @@ -1,4 +0,0 @@ -# Notes -* First Note - -# Changes diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..798aaf50 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/policies/msgraph-sdk-python-core.yml b/.github/policies/msgraph-sdk-python-core.yml new file mode 100644 index 00000000..cfd24f30 --- /dev/null +++ b/.github/policies/msgraph-sdk-python-core.yml @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# File initially created using https://github.com/MIchaelMainer/policyservicetoolkit/blob/main/branch_protection_export.ps1. + +name: msgraph-sdk-python-core-branch-protection +description: Branch protection policy for the msgraph-sdk-python-core repository +resource: repository +configuration: + branchProtectionRules: + + - branchNamePattern: dev + # This branch pattern applies to the following branches as of 06/12/2023 11:37:28: + # dev + + # Specifies whether this branch can be deleted. boolean + allowsDeletions: false + # Specifies whether forced pushes are allowed on this branch. boolean + allowsForcePushes: false + # Specifies whether new commits pushed to the matching branches dismiss pull request review approvals. boolean + dismissStaleReviews: true + # Specifies whether admins can overwrite branch protection. boolean + isAdminEnforced: false + # Indicates whether "Require a pull request before merging" is enabled. boolean + requiresPullRequestBeforeMerging: true + # Specifies the number of pull request reviews before merging. int (0-6). Should be null/empty if PRs are not required + requiredApprovingReviewsCount: 1 + # Require review from Code Owners. Requires requiredApprovingReviewsCount. boolean + requireCodeOwnersReview: true + # Are commits required to be signed. boolean. TODO: all contributors must have commit signing on local machines. + requiresCommitSignatures: false + # Are conversations required to be resolved before merging? boolean + requiresConversationResolution: true + # Are merge commits prohibited from being pushed to this branch. boolean + requiresLinearHistory: false + # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any + # existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status + requiredStatusChecks: + - CodeQL + # Require branches to be up to date before merging. Requires requiredStatusChecks. boolean + requiresStrictStatusChecks: true + # Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush. + restrictsPushes: false + # Restrict who can dismiss pull request reviews. boolean + restrictsReviewDismissals: false + + - branchNamePattern: master + # This branch pattern applies to the following branches as of 06/12/2023 11:37:28: + # master + + # Specifies whether this branch can be deleted. boolean + allowsDeletions: false + # Specifies whether forced pushes are allowed on this branch. boolean + allowsForcePushes: true + # Specifies whether new commits pushed to the matching branches dismiss pull request review approvals. boolean + dismissStaleReviews: true + # Specifies whether admins can overwrite branch protection. boolean + isAdminEnforced: false + # Indicates whether "Require a pull request before merging" is enabled. boolean + requiresPullRequestBeforeMerging: true + # Specifies the number of pull request reviews before merging. int (0-6). Should be null/empty if PRs are not required + requiredApprovingReviewsCount: 1 + # Require review from Code Owners. Requires requiredApprovingReviewsCount. boolean + requireCodeOwnersReview: true + # Are commits required to be signed. boolean. TODO: all contributors must have commit signing on local machines. + requiresCommitSignatures: false + # Are conversations required to be resolved before merging? boolean + requiresConversationResolution: true + # Are merge commits prohibited from being pushed to this branch. boolean + requiresLinearHistory: false + # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any + # existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status + requiredStatusChecks: + - CodeQL + # Require branches to be up to date before merging. Requires requiredStatusChecks. boolean + requiresStrictStatusChecks: true + # Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush. + restrictsPushes: false + # Restrict who can dismiss pull request reviews. boolean + restrictsReviewDismissals: false diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 00000000..3e33b040 --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,101 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isIssue + - isOpen + - hasLabel: + label: 'needs author feedback' + - hasLabel: + label: 'Status: No Recent Activity' + - noActivitySince: + days: 3 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isIssue + - isOpen + - hasLabel: + label: 'needs author feedback' + - noActivitySince: + days: 4 + - isNotLabeledWith: + label: 'Status: No Recent Activity' + actions: + - addLabel: + label: 'Status: No Recent Activity' + - addReply: + reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isIssue + - isOpen + - hasLabel: + label: 'duplicate' + - noActivitySince: + days: 1 + actions: + - addReply: + reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. + - closeIssue + eventResponderTasks: + - if: + - payloadType: Issue_Comment + - isAction: + action: Created + - isActivitySender: + issueAuthor: True + - hasLabel: + label: 'needs author feedback' + - isOpen + then: + - addLabel: + label: 'Needs: Attention :wave:' + - removeLabel: + label: 'needs author feedback' + description: + - if: + - payloadType: Issues + - not: + isAction: + action: Closed + - hasLabel: + label: 'Status: No Recent Activity' + then: + - removeLabel: + label: 'Status: No Recent Activity' + description: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: 'Status: No Recent Activity' + then: + - removeLabel: + label: 'Status: No Recent Activity' + description: + - if: + - payloadType: Pull_Request + then: + - inPrLabel: + label: WIP + description: +onFailure: +onSuccess: diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 00000000..2531e043 --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,30 @@ +name: Auto-merge dependabot updates + +on: + pull_request: + branches: [master] + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot-merge: + runs-on: ubuntu-latest + + if: ${{ github.actor == 'dependabot[bot]' }} + + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.6.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Enable auto-merge for Dependabot PRs + # Only if version bump is not a major version change + if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/ci.yml b/.github/workflows/build.yml similarity index 66% rename from .github/workflows/ci.yml rename to .github/workflows/build.yml index 5104b09c..e0a4536c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build.yml @@ -1,42 +1,47 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: CI +name: Build and test on: + push: pull_request: - branches: [master, dev] + workflow_call: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 40 strategy: + max-parallel: 5 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pipenv - pipenv install --dev --skip-lock + pip install -r requirements-dev.txt - name: Check code format run: | - pipenv run yapf -dr . + yapf -dr src - name: Check import order run: | - pipenv run isort . + isort src + - name: Static type checking with Mypy + run: | + mypy src - name: Lint with Pylint run: | - pipenv run pylint msgraph --disable=W --rcfile=.pylintrc + pylint src --disable=W --rcfile=.pylintrc - name: Test with pytest run: | - pipenv run pytest + pytest env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..0516477e --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,69 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [master, dev] + pull_request: + # The branches below must be a subset of the branches above + branches: [master, dev] + schedule: + - cron: "32 11 * * 6" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["python"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 \ No newline at end of file diff --git a/.github/workflows/conflicting-pr-label.yml b/.github/workflows/conflicting-pr-label.yml new file mode 100644 index 00000000..b9f9dea5 --- /dev/null +++ b/.github/workflows/conflicting-pr-label.yml @@ -0,0 +1,34 @@ +# This is a basic workflow to help you get started with Actions + +name: PullRequestConflicting + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [master, dev] + pull_request: + types: [synchronize] + branches: [master, dev] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: check if prs are dirty + uses: eps1lon/actions-label-merge-conflict@releases/2.x + if: env.LABELING_TOKEN != '' && env.LABELING_TOKEN != null + id: check + with: + dirtyLabel: "conflicting" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + continueOnMissingPermissions: true + commentOnDirty: "This pull request has conflicting changes, the author must resolve the conflicts before this pull request can be merged." + commentOnClean: "Conflicts have been resolved. A maintainer will take a look shortly." + env: + LABELING_TOKEN: ${{secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fcc9f2ee..b9d20dd2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,28 +1,51 @@ -name: Publish MsGraph-Core package to PyPI +name: Publish package to PyPI and create release on: - release: - types: - - published + push: + branches: [master] jobs: + build: + uses: ./.github/workflows/build.yml + publish: - name: Create release and publish distribution to PyPI + name: Publish distribution to PyPI runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + environment: pypi_prod + needs: [build] steps: - name: Checkout code - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 with: - python-version: 3.8 - - name: Install flit + python-version: 3.11 + - name: Install dependencies run: | - pip install flit - - name: Publish the distibution to PyPI - if: github.repository == 'microsoftgraph/msgraph-sdk-python-core' - run: flit publish + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + release: + name: Create release + runs-on: ubuntu-latest + needs: [publish] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Extract release notes + id: extract-release-notes + uses: ffurrer2/extract-release-notes@v1 + - name: Create release env: - FLIT_INDEX_URL: https://upload.pypi.org/legacy/ - FLIT_USERNAME: __token__ - FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release create --notes '${{ steps.extract-release-notes.outputs.release_notes }}' --title ${{ github.ref_name }} ${{ github.ref_name }} + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 6f82a178..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Create a release - -on: - push: - tags: - - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 - -jobs: - autorelease: - name: Create release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python 3.8 - uses: actions/setup-python@v4 - with: - python-version: 3.8 - - name: Release Notes - run: | - git log $(git describe HEAD~ --tags --abbrev=0)..HEAD --pretty='format:* %h %s%n' --no-merges >> ".github/RELEASE-TEMPLATE.md" - - name: Create Release Draft - uses: softprops/action-gh-release@v1 - if: github.repository == 'microsoftgraph/msgraph-sdk-python-core' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - with: - body_path: ".github/RELEASE-TEMPLATE.md" - draft: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 389a73a3..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,32 +0,0 @@ -default_language_version: - python: python3 -repos: - - repo: local - hooks: - - id: yapf - name: yapf - stages: [commit] - language: system - entry: pipenv run yapf -ir . - types: [python] - - - id: isort - name: isort - stages: [commit] - language: system - entry: pipenv run isort . - types: [python] - - - id: pylint - name: pylint - stages: [commit] - language: system - entry: pipenv run pylint msgraph --disable=W --rcfile=.pylintrc - types: [python] - - - id: pytest - name: pytest - stages: [commit] - language: system - entry: pipenv run pytest tests - types: [python] diff --git a/.pylintrc b/.pylintrc index ef8b4192..99ccac38 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,15 +60,9 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, +disable=long-suffix, old-ne-operator, old-octal-literal, - import-star-module-level, non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, @@ -78,74 +72,16 @@ disable=print-statement, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, too-few-public-methods, - no-self-use, missing-module-docstring, missing-class-docstring, missing-function-docstring, - C0330, + C0103, R0801, + R0904, + R0911, + # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -321,13 +257,6 @@ max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no @@ -585,4 +514,4 @@ preferred-modules= # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=BaseException, - Exception + Exception \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index fe912239..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.linting.enabled": true, - "python.linting.pylintPath": "pylint", - "editor.formatOnSave": true, - "python.formatting.provider": "yapf", - "python.linting.pylintEnabled": true, -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..45626e22 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2023-10-31 + +### Added + +### Changed +- GA release. + +## [1.0.0a6] - 2023-10-12 + +### Added + +### Changed +- Replaced default transport with graph transport when using custom client with proxy. + +## [1.0.0a5] - 2023-06-20 + +### Added + +- Added `AzureIdentityAuthenticationProvider` that sets the default scopes and allowed hosts. + +### Changed + +- Changed the documentation in the README to show how to use `AzureIdentityAuthenticationProvider` from the core SDK. + +## [1.0.0a4] - 2023-02-02 + +### Added + +### Changed + +- Enabled configuring of middleware during client creation by passing custom options in call to create with default middleware. +- Fixed a bug where the transport would check for request options even after they have been removed after final middleware execution. diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst index a0ec06fd..6257f2e7 100644 --- a/CODE_OF_CONDUCT.rst +++ b/CODE_OF_CONDUCT.rst @@ -1,4 +1,9 @@ -This project has adopted the `Microsoft Open Source Code of Conduct `_. For -more information see the `Code of Conduct FAQ `_ or contact opencode@microsoft.com -with any additional questions or comments. +# Microsoft Open Source Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6c477985..80b94218 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -172,8 +172,8 @@ To install dependencies that are necessary for development run, .. code:: - pip install -r requirements.txt - pip install -r dev_requirements.txt + pip3 install -r requirements.txt + pip3 install -r dev_requirements.txt To edit files, open them in an editor of your choice and modify them. To create a new file, use the editor of your choice and save the new file @@ -194,8 +194,8 @@ To run tests, .. code:: - python -m unittest discover tests/unit - python -m unittest discover tests/integration + python3 -m unittest discover tests/unit + python3 -m unittest discover tests/integration To run linting, diff --git a/Pipfile b/Pipfile deleted file mode 100644 index ae9bc9db..00000000 --- a/Pipfile +++ /dev/null @@ -1,19 +0,0 @@ -[[source]] # Package source -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] # Packages required to run the application -requests = "==2.28.1" - -[dev-packages] # Packages required to develop the application -coverage = "==6.5.0" -responses = "==0.22.0" -flit = "==3.7.1" -isort = "==5.10.0" -yapf = "==0.32.0" -mypy = "==0.982" -pylint = "==2.7.4" -pytest = "==7.1.3" -pre-commit = "==2.20.0" -azure-identity = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 1599ed1e..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,717 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "601461a78e3e30cde414ff321586bab559e4f5f79bcad98b7bbe4c2e76c35eac" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.9.24" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334", - "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef" - ], - "markers": "python_version ~= '3.6'", - "version": "==2.6.6" - }, - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "azure-core": { - "hashes": [ - "sha256:578ea3ae56bca48880c96797871b6c954b5ae78d10d54360182c7604dc837f25", - "sha256:b0036a0d256329e08d1278dff7df36be30031d2ec9b16c691bc61e4732f71fe0" - ], - "markers": "python_version >= '3.7'", - "version": "==1.26.0" - }, - "azure-identity": { - "hashes": [ - "sha256:c3fc800af58b857e7faf0e310376e5ef10f5dad5090914cc42ffa6d7d23b6729", - "sha256:f5eb0035ac9ceca26658b30bb2a375755c4cda61d0e3fd236b0e52ade2cb0995" - ], - "index": "pypi", - "version": "==1.11.0" - }, - "certifi": { - "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.9.24" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" - }, - "cfgv": { - "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" - }, - "colorama": { - "hashes": [ - "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", - "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" - ], - "markers": "sys_platform == 'win32' and sys_platform == 'win32'", - "version": "==0.4.5" - }, - "coverage": { - "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" - ], - "index": "pypi", - "version": "==6.5.0" - }, - "cryptography": { - "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" - ], - "markers": "python_version >= '3.6'", - "version": "==38.0.1" - }, - "distlib": { - "hashes": [ - "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", - "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" - ], - "version": "==0.3.6" - }, - "docutils": { - "hashes": [ - "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", - "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" - ], - "markers": "python_version >= '3.7'", - "version": "==0.19" - }, - "filelock": { - "hashes": [ - "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc", - "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.0" - }, - "flit": { - "hashes": [ - "sha256:06a93a6737fa9380ba85fe8d7f28efb6c93c4f4ee9c7d00cc3375a81f33b91a4", - "sha256:3c9bd9c140515bfe62dd938c6610d10d6efb9e35cc647fc614fe5fb3a5036682" - ], - "index": "pypi", - "version": "==3.7.1" - }, - "flit-core": { - "hashes": [ - "sha256:14955af340c43035dbfa96b5ee47407e377ee337f69e70f73064940d27d0a44f", - "sha256:e454fdbf68c7036e1c7435ec7479383f9d9a1650ca5b304feb184eba1efcdcef" - ], - "markers": "python_version >= '3.6'", - "version": "==3.7.1" - }, - "identify": { - "hashes": [ - "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245", - "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.6" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "isort": { - "hashes": [ - "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f", - "sha256:e52ff6d38012b131628cf0f26c51e7bd3a7c81592eefe3ac71411e692f1b9345" - ], - "index": "pypi", - "version": "==5.10.0" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" - ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "msal": { - "hashes": [ - "sha256:78344cd4c91d6134a593b5e3e45541e666e37b747ff8a6316c3668dd1e6ab6b2", - "sha256:d2f1c26368ecdc28c8657d457352faa0b81b1845a7b889d8676787721ba86792" - ], - "version": "==1.20.0" - }, - "msal-extensions": { - "hashes": [ - "sha256:91e3db9620b822d0ed2b4d1850056a0f133cba04455e62f11612e40f5502f2ee", - "sha256:c676aba56b0cce3783de1b5c5ecfe828db998167875126ca4b47dc6436451354" - ], - "version": "==1.0.0" - }, - "mypy": { - "hashes": [ - "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d", - "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24", - "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046", - "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e", - "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3", - "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5", - "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20", - "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda", - "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1", - "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146", - "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206", - "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746", - "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6", - "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e", - "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc", - "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a", - "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8", - "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763", - "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2", - "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947", - "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40", - "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b", - "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795", - "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c" - ], - "index": "pypi", - "version": "==0.982" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "nodeenv": { - "hashes": [ - "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", - "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.7.0" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "platformdirs": { - "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.2" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "portalocker": { - "hashes": [ - "sha256:400bae275366e7b840d4baad0654c6ec5994e07c40c423d78e9e1340279b8352", - "sha256:ae8e9cc2660da04bf41fa1a0eef7e300bb5e4a5869adfb1a6d8551632b559b2b" - ], - "markers": "python_version >= '3.5' and platform_system == 'Windows'", - "version": "==2.5.1" - }, - "pre-commit": { - "hashes": [ - "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7", - "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959" - ], - "index": "pypi", - "version": "==2.20.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "pyjwt": { - "extras": [ - "crypto" - ], - "hashes": [ - "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80", - "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.0" - }, - "pylint": { - "hashes": [ - "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a", - "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee" - ], - "index": "pypi", - "version": "==2.7.4" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", - "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" - ], - "index": "pypi", - "version": "==7.1.3" - }, - "pywin32": { - "hashes": [ - "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc", - "sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269", - "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3", - "sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48", - "sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4", - "sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96", - "sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df", - "sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1", - "sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43", - "sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9", - "sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f", - "sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988", - "sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e", - "sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5" - ], - "markers": "platform_system == 'Windows'", - "version": "==304" - }, - "pyyaml": { - "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "responses": { - "hashes": [ - "sha256:396acb2a13d25297789a5866b4881cf4e46ffd49cc26c43ab1117f40b973102e", - "sha256:dcf294d204d14c436fddcc74caefdbc5764795a40ff4e6a7740ed8ddbf3294be" - ], - "index": "pypi", - "version": "==0.22.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "tomli-w": { - "hashes": [ - "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463", - "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.0" - }, - "types-cryptography": { - "hashes": [ - "sha256:913b3e66a502edbf4bfc3bb45e33ab476040c56942164a7ff37bd1f0ef8ef783", - "sha256:b85c45fd4d3d92e8b18e9a5ee2da84517e8fff658e3ef5755c885b1c2a27c1fe" - ], - "version": "==3.3.23" - }, - "types-toml": { - "hashes": [ - "sha256:8300fd093e5829eb9c1fba69cee38130347d4b74ddf32d0a7df650ae55c2b599", - "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a" - ], - "version": "==0.10.8" - }, - "typing-extensions": { - "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" - ], - "markers": "python_version >= '3.7'", - "version": "==4.4.0" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - }, - "virtualenv": { - "hashes": [ - "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da", - "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27" - ], - "markers": "python_version >= '3.6'", - "version": "==20.16.5" - }, - "wrapt": { - "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" - ], - "version": "==1.12.1" - }, - "yapf": { - "hashes": [ - "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32", - "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b" - ], - "index": "pypi", - "version": "==0.32.0" - } - } -} diff --git a/README.md b/README.md index 8b206244..b110c62f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ -[![CI Actions Status](https://github.com/microsoftgraph/msgraph-sdk-python-core/workflows/msgraph-sdk-python-core/badge.svg)](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions) +[![PyPI version](https://badge.fury.io/py/msgraph-core.svg)](https://badge.fury.io/py/msgraph-core) +[![CI Actions Status](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions/workflows/build.yml/badge.svg)](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions/workflows/build.yml) [![Downloads](https://pepy.tech/badge/msgraph-core)](https://pepy.tech/project/msgraph-core) -## Microsoft Graph Core Python Client Library (preview). -The Microsoft Graph Core Python client library is a lightweight wrapper around the Microsoft Graph API. It provides functionality to create clients with desired configuration and middleware. +## Microsoft Graph Core Python Client Library. -**Disclaimer**: Please, be aware that preview versions of `msgraph-core` package are for testing purpose only. Do not use them in a production environment. +The Microsoft Graph Core Python Client Library contains core classes used by [Microsoft Graph Python Client Library](https://github.com/microsoftgraph/msgraph-sdk-python) to send native HTTP requests to [Microsoft Graph API](https://graph.microsoft.com). -> Note: -> This is not the most recent version of the Python Core library. Upgrading to the [newer version](https://github.com/microsoftgraph/msgraph-sdk-python-core/blob/kiota/long-term-branch/README.md) will introduce breaking changes into your application. +> NOTE: +> This is a new major version of the Python Core library for Microsoft Graph based on the [Kiota](https://microsoft.github.io/kiota/) project. We recommend to use this library with the [full Python SDK](https://github.com/microsoftgraph/msgraph-sdk-python). +> Upgrading to this version from the [previous version of the Python Core library](https://pypi.org/project/msgraph-core/0.2.2/) will introduce breaking changes into your application. ## Prerequisites - Python 3.5+ (this library doesn't support older versions of Python) + Python 3.8+ + +This library doesn't support [older](https://devguide.python.org/versions/) versions of Python. ## Getting started @@ -19,51 +22,59 @@ The Microsoft Graph Core Python client library is a lightweight wrapper around t To call Microsoft Graph, your app must acquire an access token from the Microsoft identity platform. Learn more about this - -- [Authentication and authorization basics for Microsoft Graph](https://docs.microsoft.com/en-us/graph/auth/auth-concepts) -- [Register your app with the Microsoft identity platform](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) - +- [Authentication and authorization basics for Microsoft Graph](https://docs.microsoft.com/en-us/graph/auth/auth-concepts) +- [Register your app with the Microsoft identity platform](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) ### 2. Install the required packages msgraph-core is available on PyPI. ```cmd -python -m pip install msgraph-core -python -m pip install azure-identity +pip3 install msgraph-core +pip3 install azure-identity ``` -### 3. Import modules +### 3. Configure an Authentication Provider Object -```python -from azure.identity import InteractiveBrowserCredential -from msgraph.core import GraphClient -``` +An instance of the `BaseGraphRequestAdapter` class handles building client. To create a new instance of this class, you need to provide an instance of `AuthenticationProvider`, which can authenticate requests to Microsoft Graph. -### 4. Configure a Credential Object +> **Note**: This client library offers an asynchronous API by default. Async is a concurrency model that is far more efficient than multi-threading, and can provide significant performance benefits and enable the use of long-lived network connections such as WebSockets. We support popular python async environments such as `asyncio`, `anyio` or `trio`. For authentication you need to use one of the async credential classes from `azure.identity`. -```python -# Using InteractiveBrowserCredential for demonstration purposes. +```py +# Using EnvironmentCredential for demonstration purposes. # There are many other options for getting an access token. See the following for more information. -# https://pypi.org/project/azure-identity/ +# https://pypi.org/project/azure-identity/#async-credentials +from azure.identity.aio import EnvironmentCredential +from msgraph_core.authentication import AzureIdentityAuthenticationProvider -browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') +credential=EnvironmentCredential() +auth_provider = AzureIdentityAuthenticationProvider(credential) ``` -### 5. Pass the credential object to the GraphClient constructor. +> **Note**: `AzureIdentityAuthenticationProvider` sets the default scopes and allowed hosts. + +### 5. Pass the authentication provider object to the BaseGraphRequestAdapter constructor. ```python -client = GraphClient(credential=browser_credential) +from msgraph_core import BaseGraphRequestAdapter +adapter = BaseGraphRequestAdapter(auth_provider) ``` -### 6. Make a requests to the graph API using the client +### 6. Make a requests to the graph. + +After you have a `BaseGraphRequestAdapter` that is authenticated, you can begin making calls against the service. ```python -result = client.get('/me') -print(result.json()) -``` +import asyncio +from kiota_abstractions.request_information import RequestInformation -For more information on how to use the package, refer to the [samples](https://github.com/microsoftgraph/msgraph-sdk-python-core/tree/dev/samples). +request_info = RequestInformation() +request_info.url = 'https://graph.microsoft.com/v1.0/me' +# User is your own type that implements Parsable or comes from the service library +user = asyncio.run(adapter.send_async(request_info, User, {})) +print(user.display_name) +``` ## Telemetry Metadata @@ -82,5 +93,3 @@ Please see the [contributing guidelines](CONTRIBUTING.rst). Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT [license](LICENSE). This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - - diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..eaf439ae --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,25 @@ +# TODO: The maintainer of this repo has not yet edited this file + +**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? + +- **No CSS support:** Fill out this template with information about how to file issues and get help. +- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. +- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. + +*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* + +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE +FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER +CHANNEL. WHERE WILL YOU HELP PEOPLE?**. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py deleted file mode 100644 index e726d590..00000000 --- a/msgraph/core/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._client_factory import HTTPClientFactory -from ._constants import SDK_VERSION -from ._enums import APIVersion, NationalClouds -from ._graph_client import GraphClient - -__version__ = SDK_VERSION diff --git a/msgraph/core/_client_factory.py b/msgraph/core/_client_factory.py deleted file mode 100644 index 211ff5b1..00000000 --- a/msgraph/core/_client_factory.py +++ /dev/null @@ -1,95 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import functools - -from requests import Session - -from ._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from ._enums import APIVersion, NationalClouds -from .middleware.abc_token_credential import TokenCredential -from .middleware.authorization import AuthorizationHandler -from .middleware.middleware import BaseMiddleware, MiddlewarePipeline -from .middleware.retry import RetryHandler -from .middleware.telemetry import TelemetryHandler - - -class HTTPClientFactory: - """Constructs native HTTP Client(session) instances configured with either custom or default - pipeline of middleware. - - :func: Class constructor accepts a user provided session object and kwargs to configure the - request handling behaviour of the client - :keyword enum api_version: The Microsoft Graph API version to be used, for example - `APIVersion.v1` (default). This value is used in setting the base url for all requests for - that session. - :class:`~msgraphcore.enums.APIVersion` defines valid API versions. - :keyword enum cloud: a supported Microsoft Graph cloud endpoint. - Defaults to `NationalClouds.Global` - :class:`~msgraphcore.enums.NationalClouds` defines supported sovereign clouds. - :keyword tuple timeout: Default connection and read timeout values for all session requests. - Specify a tuple in the form of Tuple(connect_timeout, read_timeout) if you would like to set - the values separately. If you specify a single value for the timeout, the timeout value will - be applied to both the connect and the read timeouts. - :keyword obj session: A custom Session instance from the python requests library - """ - - def __init__(self, **kwargs): - """Class constructor that accepts a user provided session object and kwargs - to configure the request handling behaviour of the client""" - self.api_version = kwargs.get('api_version', APIVersion.v1) - self.endpoint = kwargs.get('cloud', NationalClouds.Global) - self.timeout = kwargs.get('timeout', (DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT)) - self.session = kwargs.get('session', Session()) - - self._set_base_url() - self._set_default_timeout() - - def create_with_default_middleware(self, credential: TokenCredential, **kwargs) -> Session: - """Applies the default middleware chain to the HTTP Client - - :param credential: TokenCredential used to acquire an access token for the Microsoft - Graph API. Created through one of the credential classes from `azure.identity` - """ - middleware = [ - AuthorizationHandler(credential, **kwargs), - RetryHandler(**kwargs), - TelemetryHandler(), - ] - self._register(middleware) - return self.session - - def create_with_custom_middleware(self, middleware: [BaseMiddleware]) -> Session: - """Applies a custom middleware chain to the HTTP Client - - :param list middleware: Custom middleware(HTTPAdapter) list that will be used to create - a middleware pipeline. The middleware should be arranged in the order in which they will - modify the request. - """ - if not middleware: - raise ValueError("Please provide a list of custom middleware") - self._register(middleware) - return self.session - - def _set_base_url(self): - """Helper method to set the base url""" - base_url = self.endpoint + '/' + self.api_version - self.session.base_url = base_url - - def _set_default_timeout(self): - """Helper method to set a default timeout for the session - Reference: https://github.com/psf/requests/issues/2011 - """ - self.session.request = functools.partial(self.session.request, timeout=self.timeout) - - def _register(self, middleware: [BaseMiddleware]) -> None: - """ - Helper method that constructs a middleware_pipeline with the specified middleware - """ - if middleware: - middleware_pipeline = MiddlewarePipeline() - for ware in middleware: - middleware_pipeline.add_middleware(ware) - - self.session.mount('https://', middleware_pipeline) diff --git a/msgraph/core/_graph_client.py b/msgraph/core/_graph_client.py deleted file mode 100644 index 37779166..00000000 --- a/msgraph/core/_graph_client.py +++ /dev/null @@ -1,177 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json - -from requests import Request, Session - -from ._client_factory import HTTPClientFactory -from .middleware.request_context import RequestContext - -# These are middleware options that can be configured per request. -# Supports options for default middleware as well as custom middleware. -supported_options = [ - # Auth Options - 'scopes', - - # Retry Options - 'max_retries', - 'retry_backoff_factor', - 'retry_backoff_max', - 'retry_time_limit', - 'retry_on_status_codes', - - # Custom middleware options - 'custom_option', -] - - -def collect_options(func): - """Collect middleware options into a middleware control dict and pass it as a header""" - - def wrapper(*args, **kwargs): - - middleware_control = dict() - - for option in supported_options: - value = kwargs.pop(option, None) - if value: - middleware_control.update({option: value}) - - if 'headers' in kwargs.keys(): - kwargs['headers'].update({'middleware_control': json.dumps(middleware_control)}) - else: - kwargs['headers'] = {'middleware_control': json.dumps(middleware_control)} - - return func(*args, **kwargs) - - return wrapper - - -class GraphClient: - """Constructs a custom HTTPClient to be used for requests against Microsoft Graph - - :keyword credential: TokenCredential used to acquire an access token for the Microsoft - Graph API. Created through one of the credential classes from `azure.identity` - :keyword list middleware: Custom middleware(HTTPAdapter) list that will be used to create - a middleware pipeline. The middleware should be arranged in the order in which they will - modify the request. - :keyword enum api_version: The Microsoft Graph API version to be used, for example - `APIVersion.v1` (default). This value is used in setting the base url for all requests for - that session. - :class:`~msgraphcore.enums.APIVersion` defines valid API versions. - :keyword enum cloud: a supported Microsoft Graph cloud endpoint. - Defaults to `NationalClouds.Global` - :class:`~msgraphcore.enums.NationalClouds` defines supported sovereign clouds. - :keyword tuple timeout: Default connection and read timeout values for all session requests. - Specify a tuple in the form of Tuple(connect_timeout, read_timeout) if you would like to set - the values separately. If you specify a single value for the timeout, the timeout value will - be applied to both the connect and the read timeouts. - :keyword obj session: A custom Session instance from the python requests library - """ - __instance = None - - def __new__(cls, *args, **kwargs): - if not GraphClient.__instance: - GraphClient.__instance = object.__new__(cls) - return GraphClient.__instance - - def __init__(self, **kwargs): - """ - Class constructor that accepts a session object and kwargs to - be passed to the HTTPClientFactory - """ - self.graph_session = self.get_graph_session(**kwargs) - - @collect_options - def get(self, url: str, **kwargs): - r"""Sends a GET request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.get(self._graph_url(url), **kwargs) - - def options(self, url, **kwargs): - r"""Sends a OPTIONS request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.options(self._graph_url(url), **kwargs) - - def head(self, url, **kwargs): - r"""Sends a HEAD request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.head(self._graph_url(url), **kwargs) - - def post(self, url, data=None, json=None, **kwargs): - r"""Sends a POST request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param json: (optional) json to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.post(self._graph_url(url), data=data, json=json, **kwargs) - - def put(self, url, data=None, **kwargs): - r"""Sends a PUT request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.put(self._graph_url(url), data=data, **kwargs) - - def patch(self, url, data=None, **kwargs): - r"""Sends a PATCH request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.patch(self._graph_url(url), data=data, **kwargs) - - def delete(self, url, **kwargs): - r"""Sends a DELETE request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.delete(self._graph_url(url), **kwargs) - - def _graph_url(self, url: str) -> str: - """Appends BASE_URL to user provided path - :param url: user provided path - :return: graph_url - """ - return self.graph_session.base_url + url if (url[0] == '/') else url - - @staticmethod - def get_graph_session(**kwargs): - """Method to always return a single instance of a HTTP Client""" - - credential = kwargs.pop('credential', None) - middleware = kwargs.pop('middleware', None) - - if credential and middleware: - raise ValueError( - "Invalid parameters! Both TokenCredential and middleware cannot be passed" - ) - if not credential and not middleware: - raise ValueError("Invalid parameters!. Missing TokenCredential or middleware") - - if credential: - return HTTPClientFactory(**kwargs).create_with_default_middleware(credential, **kwargs) - return HTTPClientFactory(**kwargs).create_with_custom_middleware(middleware) diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py deleted file mode 100644 index b74cfa3b..00000000 --- a/msgraph/core/middleware/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ diff --git a/msgraph/core/middleware/abc_token_credential.py b/msgraph/core/middleware/abc_token_credential.py deleted file mode 100644 index 6bc80bcd..00000000 --- a/msgraph/core/middleware/abc_token_credential.py +++ /dev/null @@ -1,12 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from abc import ABC, abstractmethod - - -class TokenCredential(ABC): - - @abstractmethod - def get_token(self, *scopes, **kwargs): - pass diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py deleted file mode 100644 index 2e2ed5af..00000000 --- a/msgraph/core/middleware/authorization.py +++ /dev/null @@ -1,37 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from .._enums import FeatureUsageFlag -from .abc_token_credential import TokenCredential -from .middleware import BaseMiddleware - - -class AuthorizationHandler(BaseMiddleware): - - def __init__(self, credential: TokenCredential, **kwargs): - super().__init__() - self.credential = credential - self.scopes = kwargs.get("scopes", ['.default']) - self.retry_count = 0 - - def send(self, request, **kwargs): - context = request.context - request.headers.update( - {'Authorization': 'Bearer {}'.format(self._get_access_token(context))} - ) - context.set_feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED - response = super().send(request, **kwargs) - - # Token might have expired just before transmission, retry the request one more time - if response.status_code == 401 and self.retry_count < 2: - self.retry_count += 1 - return self.send(request, **kwargs) - return response - - def _get_access_token(self, context): - return self.credential.get_token(*self.get_scopes(context))[0] - - def get_scopes(self, context): - # Checks if there are any options for this middleware - return context.middleware_control.get('scopes', self.scopes) diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py deleted file mode 100644 index ee4b84aa..00000000 --- a/msgraph/core/middleware/middleware.py +++ /dev/null @@ -1,68 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json -import ssl - -from requests.adapters import HTTPAdapter -from urllib3 import PoolManager - -from .request_context import RequestContext - - -class MiddlewarePipeline(HTTPAdapter): - """MiddlewarePipeline, entry point of middleware - The pipeline is implemented as a linked-list, read more about - it here https://buffered.dev/middleware-python-requests/ - """ - - def __init__(self): - super().__init__() - self._current_middleware = None - self._first_middleware = None - self.poolmanager = PoolManager(ssl_version=ssl.PROTOCOL_TLSv1_2) - - def add_middleware(self, middleware): - if self._middleware_present(): - self._current_middleware.next = middleware - self._current_middleware = middleware - else: - self._first_middleware = middleware - self._current_middleware = self._first_middleware - - def send(self, request, **kwargs): - - middleware_control_json = request.headers.pop('middleware_control', None) - if middleware_control_json: - middleware_control = json.loads(middleware_control_json) - else: - middleware_control = dict() - - request.context = RequestContext(middleware_control, request.headers) - - if self._middleware_present(): - return self._first_middleware.send(request, **kwargs) - # No middleware in pipeline, call superclass' send - return super().send(request, **kwargs) - - def _middleware_present(self): - return self._current_middleware - - -class BaseMiddleware(HTTPAdapter): - """Base class for middleware - - Handles moving a Request to the next middleware in the pipeline. - If the current middleware is the last one in the pipeline, it - makes a network request - """ - - def __init__(self): - super().__init__() - self.next = None - - def send(self, request, **kwargs): - if self.next is None: - return super().send(request, **kwargs) - return self.next.send(request, **kwargs) diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py deleted file mode 100644 index 814b27a8..00000000 --- a/msgraph/core/middleware/retry.py +++ /dev/null @@ -1,221 +0,0 @@ -import datetime -import random -import time -from email.utils import parsedate_to_datetime - -from .._enums import FeatureUsageFlag -from .middleware import BaseMiddleware - - -class RetryHandler(BaseMiddleware): - """ - TransportAdapter that allows us to specify the retry policy for all requests - - Retry configuration. - - :param int max_retries: - Maximum number of retries to allow. Takes precedence over other counts. - Set to ``0`` to fail on the first retry. - :param iterable retry_on_status_codes: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``RETRY STATUS CODES``. - :param float retry_backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). - The request will sleep for:: - {backoff factor} * (2 ** ({retry number} - 1)) - seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep - for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer - than :attr:`RetryHandler.MAXIMUM_BACKOFF`. - By default, backoff is set to 0.5. - :param int retry_time_limit: - The maximum cumulative time in seconds that total retries should take. - The cumulative retry time and retry-after value for each request retry - will be evaluated against this value; if the cumulative retry time plus - the retry-after value is greater than the retry_time_limit, the failed - response will be immediately returned, else the request retry continues. - """ - - DEFAULT_MAX_RETRIES = 3 - MAX_RETRIES = 10 - DEFAULT_DELAY = 3 - MAX_DELAY = 180 - DEFAULT_BACKOFF_FACTOR = 0.5 - MAXIMUM_BACKOFF = 120 - _DEFAULT_RETRY_STATUS_CODES = {429, 503, 504} - - def __init__(self, **kwargs): - super().__init__() - self.max_retries: int = min( - kwargs.pop('max_retries', self.DEFAULT_MAX_RETRIES), self.MAX_RETRIES - ) - self.backoff_factor: float = kwargs.pop('retry_backoff_factor', self.DEFAULT_BACKOFF_FACTOR) - self.backoff_max: int = kwargs.pop('retry_backoff_max', self.MAXIMUM_BACKOFF) - self.timeout: int = kwargs.pop('retry_time_limit', self.MAX_DELAY) - - status_codes: [int] = kwargs.pop('retry_on_status_codes', []) - - self._retry_on_status_codes: set = set(status_codes) | self._DEFAULT_RETRY_STATUS_CODES - self._allowed_methods: set = frozenset( - ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] - ) - self._respect_retry_after_header: bool = True - - @classmethod - def disable_retries(cls): - """ - Disable retries by setting retry_total to zero. - retry_total takes precedence over all other counts. - """ - return cls(max_retries=0) - - def get_retry_options(self, middleware_control): - """ - Check if request specific configs have been passed and override any session defaults - Then configure retry settings into the form of a dict. - """ - if middleware_control: - return { - 'total': - min(middleware_control.get('max_retries', self.max_retries), self.MAX_RETRIES), - 'backoff': - middleware_control.get('retry_backoff_factor', self.backoff_factor), - 'max_backoff': - middleware_control.get('retry_backoff_max', self.backoff_max), - 'timeout': - middleware_control.get('retry_time_limit', self.timeout), - 'retry_codes': - set(middleware_control.get('retry_on_status_codes', self._retry_on_status_codes)) - | set(self._DEFAULT_RETRY_STATUS_CODES), - 'methods': - self._allowed_methods, - } - return { - 'total': self.max_retries, - 'backoff': self.backoff_factor, - 'max_backoff': self.backoff_max, - 'timeout': self.timeout, - 'retry_codes': self._retry_on_status_codes, - 'methods': self._allowed_methods, - } - - def send(self, request, **kwargs): - """ - Sends the http request object to the next middleware or retries the request if necessary. - """ - retry_options = self.get_retry_options(request.context.middleware_control) - absolute_time_limit = min(retry_options['timeout'], self.MAX_DELAY) - response = None - retry_count = 0 - retry_valid = True - - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': '{}'.format(retry_count)}) - response = super().send(request, **kwargs) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(retry_options, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_options, retry_count) - - # Get the delay time between retries - delay = self.get_delay_time(retry_options, retry_count, response) - - if retry_valid and delay < absolute_time_limit: - time.sleep(delay) - end_time = time.time() - absolute_time_limit -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - - continue - break - return response - - def should_retry(self, retry_options, response): - """ - Determines whether the request should be retried - Checks if the request method is in allowed methods - Checks if the response status code is in retryable status codes. - """ - if not self._is_method_retryable(retry_options, response.request): - return False - if not self._is_request_payload_buffered(response): - return False - return retry_options['total'] and response.status_code in retry_options['retry_codes'] - - def _is_method_retryable(self, retry_options, request): - """ - Checks if a given request should be retried upon, depending on - whether the HTTP method is in the set of allowed methods - """ - if request.method.upper() not in retry_options['methods']: - return False - return True - - def _is_request_payload_buffered(self, response): - """ - Checks if the request payload is buffered/rewindable. - Payloads with forward only streams will return false and have the responses - returned without any retry attempt. - """ - if response.request.method.upper() in frozenset(['HEAD', 'GET', 'DELETE', 'OPTIONS']): - return True - if response.request.headers.get('Content-Type') == "application/octet-stream": - return False - return True - - def check_retry_valid(self, retry_options, retry_count): - """ - Check that the max retries limit has not been hit - """ - if retry_count < retry_options['total']: - return True - return False - - def get_delay_time(self, retry_options, retry_count, response=None): - """ - Get the time in seconds to delay between retry attempts. - Respects a retry-after header in the response if provided - If no retry-after response header, it defaults to exponential backoff - """ - retry_after = self._get_retry_after(response) - if retry_after: - return retry_after - return self._get_delay_time_exp_backoff(retry_options, retry_count) - - def _get_delay_time_exp_backoff(self, retry_options, retry_count): - """ - Get time in seconds to delay between retry attempts based on an exponential - backoff value. - """ - exp_backoff_value = retry_options['backoff'] * +(2**(retry_count - 1)) - backoff_value = exp_backoff_value + (random.randint(0, 1000) / 1000) - - backoff = min(retry_options['max_backoff'], backoff_value) - return backoff - - def _get_retry_after(self, response): - """ - Check if retry-after is specified in the response header and get the value - """ - retry_after = response.headers.get("retry-after") - if retry_after: - return self._parse_retry_after(retry_after) - return None - - def _parse_retry_after(self, retry_after): - """ - Helper to parse Retry-After and get value in seconds. - """ - try: - delay = int(retry_after) - except ValueError: - # Not an integer? Try HTTP date - retry_date = parsedate_to_datetime(retry_after) - delay = (retry_date - datetime.datetime.now(retry_date.tzinfo)).total_seconds() - return max(0, delay) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py deleted file mode 100644 index f7151db6..00000000 --- a/msgraph/core/middleware/telemetry.py +++ /dev/null @@ -1,86 +0,0 @@ -import platform - -from urllib3.util import parse_url - -from .._constants import SDK_VERSION -from .._enums import NationalClouds -from .middleware import BaseMiddleware - - -class TelemetryHandler(BaseMiddleware): - """Middleware component that attaches metadata to a Graph request in order to help - the SDK team improve the developer experience. - """ - - def send(self, request, **kwargs): - - if self.is_graph_url(request.url): - self._add_client_request_id_header(request) - self._append_sdk_version_header(request) - self._add_host_os_header(request) - self._add_runtime_environment_header(request) - - response = super().send(request, **kwargs) - return response - - def is_graph_url(self, url): - """Check if the request is made to a graph endpoint. We do not add telemetry headers to - non-graph endpoints""" - endpoints = set(item.value for item in NationalClouds) - - base_url = parse_url(url) - endpoint = "{}://{}".format( - base_url.scheme, - base_url.netloc, - ) - return endpoint in endpoints - - def _add_client_request_id_header(self, request) -> None: - """Add a client-request-id header with GUID value to request""" - request.headers.update( - {'client-request-id': '{}'.format(request.context.client_request_id)} - ) - - def _append_sdk_version_header(self, request) -> None: - """Add SdkVersion request header to each request to identify the language and - version of the client SDK library(s). - Also adds the featureUsage value. - """ - if 'sdkVersion' in request.headers: - sdk_version = request.headers.get('sdkVersion') - if not sdk_version == f'graph-python-core/{SDK_VERSION} '\ - f'(featureUsage={request.context.feature_usage})': - request.headers.update( - { - 'sdkVersion': - f'graph-python-core/{SDK_VERSION},{ sdk_version} '\ - f'(featureUsage={request.context.feature_usage})' - } - ) - else: - request.headers.update( - { - 'sdkVersion': - f'graph-python-core/{SDK_VERSION} '\ - f'(featureUsage={request.context.feature_usage})' - } - ) - - def _add_host_os_header(self, request) -> None: - """ - Add HostOS request header to each request to help identify the OS - on which our client SDK is running on - """ - system = platform.system() - version = platform.version() - host_os = f'{system} {version}' - request.headers.update({'HostOs': host_os}) - - def _add_runtime_environment_header(self, request) -> None: - """ - Add RuntimeEnvironment request header to capture the runtime framework - on which the client SDK is running on. - """ - python_version = platform.python_version() - runtime_environment = f'Python/{python_version}' - request.headers.update({'RuntimeEnvironment': runtime_environment}) diff --git a/pyproject.toml b/pyproject.toml index c028c276..84d9ea3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,39 +1,42 @@ [build-system] -requires = ["flit_core >=3.5,<4", "requests >= 2.27.0"] -build-backend = "flit_core.buildapi" +requires = ["setuptools>=65.5.0", "wheel"] +build-backend = "setuptools.build_meta" [project] name = "msgraph-core" +version = "1.0.0" authors = [{name = "Microsoft", email = "graphtooling+python@microsoft.com"}] +description = "Core component of the Microsoft Graph Python SDK" dependencies = [ - "requests >= 2.27.0", + "microsoft-kiota-abstractions >=1.0.0,<2.0.0", + "microsoft-kiota-authentication-azure >=1.0.0,<2.0.0", + "microsoft-kiota-http >=1.0.0,<2.0.0", + "httpx[http2] >=0.23.0", ] +requires-python = ">=3.8" license = {file = "LICENSE"} readme = "README.md" keywords = ["msgraph", "openAPI", "Microsoft", "Graph"] classifiers = [ - "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", ] -dynamic = ["version", "description"] + +[project.optional-dependencies] +dev = ["yapf", "bumpver", "isort", "pylint", "pytest", "mypy"] [project.urls] homepage = "https://github.com/microsoftgraph/msgraph-sdk-python-core#readme" repository = "https://github.com/microsoftgraph/msgraph-sdk-python-core" documentation = "https://github.com/microsoftgraph/msgraph-sdk-python-core/docs" -[tool.flit.module] -name = "msgraph" - [tool.mypy] warn_unused_configs = true -files = "msgraph" +files = "src" ignore_missing_imports = true [tool.yapf] @@ -44,3 +47,20 @@ column_limit = 100 [tool.isort] profile = "hug" + +[tool.pytest.ini_options] +pythonpath = [ + "src" +] + +[tool.bumpver] +current_version = "1.0.0" +version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" +commit_message = "bump version {old_version} -> {new_version}" +commit = true +tag = false +push = false + +[tool.bumpver.file_patterns] +"pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"'] +"src/msgraph_core/_constants.py" = ["{version}"] \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..5058bb99 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,156 @@ +-i https://pypi.org/simple + +astroid==3.0.2 ; python_full_version >= '3.7.2' + +async-generator==1.10 ; python_version >= '3.5' + +asyncmock==0.4.2 + +attrs==23.2.0 ; python_version >= '3.7' + +azure-core==1.29.6 ; python_version >= '3.7' + +azure-identity==1.15.0 + +build==1.0.3 + +bumpver==2023.1129 + +certifi==2023.11.17 ; python_version >= '3.6' + +cffi==1.16.0 ; os_name == 'nt' and implementation_name != 'pypy' + +charset-normalizer==3.3.2 ; python_full_version >= '3.7.0' + +click==8.1.7 ; python_version >= '3.6' + +colorama==0.4.6 ; os_name == 'nt' + +coverage[toml]==7.4.0 ; python_version >= '3.7' + +cryptography==41.0.7 ; python_version >= '3.7' + +dill==0.3.6 ; python_version < '3.11' + +exceptiongroup==1.1.1 ; python_version < '3.11' + +idna==3.6 ; python_version >= '3.5' + +importlib-metadata==6.8.0 ; python_version >= '3.7' + +iniconfig==2.0.0 ; python_version >= '3.7' + +isort==5.13.2 + +lazy-object-proxy==1.10.0 ; python_version >= '3.7' + +lexid==2021.1006 ; python_version >= '2.7' + +looseversion==1.3.0 ; python_version >= '3.5' + +mccabe==0.7.0 ; python_version >= '3.6' + +mock==5.1.0 ; python_version >= '3.6' + +msal==1.26.0 + +msal-extensions==1.1.0 + +mypy==1.8.0 + +mypy-extensions==1.0.0 ; python_version >= '3.5' + +outcome==1.3.0.post0 ; python_version >= '3.7' + +packaging==23.2 ; python_version >= '3.7' + +pathlib2==2.3.7.post1 + +platformdirs==4.1.0 ; python_version >= '3.7' + +pluggy==1.3.0 ; python_version >= '3.7' + +portalocker==2.8.2 ; python_version >= '3.5' and platform_system == 'Windows' + +pycparser==2.21 + +pyjwt[crypto]==2.8.0 ; python_version >= '3.7' + +pylint==3.0.3 + +pyproject-hooks==1.0.0 ; python_version >= '3.7' + +pytest==7.4.4 + +pytest-cov==4.1.0 + +pytest-mock==3.12.0 + +pytest-trio==0.8.0 + +pywin32==306 ; platform_system == 'Windows' + +requests==2.31.0 ; python_version >= '3.7' + +setuptools==69.0.3 + +six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' + +sniffio==1.3.0 ; python_version >= '3.7' + +sortedcontainers==2.4.0 + +toml==0.10.2 + +tomli==2.0.1 ; python_version < '3.11' + +tomlkit==0.12.3 ; python_version >= '3.7' + +trio==0.24.0 + +types-python-dateutil==2.8.19.20240106 + +typing-extensions==4.9.0 ; python_version >= '3.7' + +urllib3==2.1.0 ; python_version >= '3.7' + +wrapt==1.15.0 ; python_version < '3.11' + +yapf==0.40.2 + +zipp==3.17.0 ; python_version >= '3.7' + +aiohttp==3.9.1 ; python_version >= '3.6' + +aiosignal==1.3.1 ; python_version >= '3.7' + +anyio==4.2.0 ; python_version >= '3.7' + +async-timeout==4.0.3 ; python_version >= '3.6' + +frozenlist==1.4.1 ; python_version >= '3.7' + +h11==0.14.0 ; python_version >= '3.7' + +h2==4.1.0 + +hpack==4.0.0 ; python_full_version >= '3.6.1' + +httpcore==1.0.2 ; python_version >= '3.7' + +httpx[http2]==0.26.0 + +hyperframe==6.0.1 ; python_full_version >= '3.6.1' + +microsoft-kiota-abstractions==1.0.0 + +microsoft-kiota-authentication-azure==1.0.0 + +microsoft-kiota-http==1.2.0 + +multidict==6.0.4 ; python_version >= '3.7' + +uritemplate==4.1.1 ; python_version >= '3.6' + +yarl==1.9.4 ; python_version >= '3.7' + diff --git a/samples/client_factory_samples.py b/samples/client_factory_samples.py index cecbb4f3..4806fb81 100644 --- a/samples/client_factory_samples.py +++ b/samples/client_factory_samples.py @@ -13,9 +13,8 @@ # This sample uses InteractiveBrowserCredential only for demonstration. # Any azure-identity TokenCredential class will work the same. from azure.identity import InteractiveBrowserCredential -from requests import Session - from msgraph.core import APIVersion, HTTPClientFactory, NationalClouds +from requests import Session scopes = ['user.read'] browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') diff --git a/samples/graph_client_samples.py b/samples/graph_client_samples.py index 4deea2e8..776b451f 100644 --- a/samples/graph_client_samples.py +++ b/samples/graph_client_samples.py @@ -10,9 +10,8 @@ # This sample uses InteractiveBrowserCredential only for demonstration. # Any azure-identity TokenCredential class will work the same. from azure.identity import InteractiveBrowserCredential -from requests import Session - from msgraph.core import APIVersion, GraphClient, NationalClouds +from requests import Session scopes = ['user.read'] browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') diff --git a/samples/retry_handler_samples.py b/samples/retry_handler_samples.py index 758c6f11..752f8f52 100644 --- a/samples/retry_handler_samples.py +++ b/samples/retry_handler_samples.py @@ -8,7 +8,6 @@ from pprint import pprint from azure.identity import InteractiveBrowserCredential - from msgraph.core import GraphClient, HTTPClientFactory scopes = ['user.read'] diff --git a/msgraph/__init__.py b/src/msgraph_core/__init__.py similarity index 60% rename from msgraph/__init__.py rename to src/msgraph_core/__init__.py index 4080dc1f..148520a3 100644 --- a/msgraph/__init__.py +++ b/src/msgraph_core/__init__.py @@ -8,6 +8,10 @@ """ Core component of the Microsoft Graph Python SDK consisting of HTTP/Graph Client and a configurable middleware pipeline (Preview). """ -from .core import SDK_VERSION +from ._constants import SDK_VERSION +from ._enums import APIVersion, NationalClouds +from .authentication import AzureIdentityAuthenticationProvider +from .base_graph_request_adapter import BaseGraphRequestAdapter +from .graph_client_factory import GraphClientFactory __version__ = SDK_VERSION diff --git a/msgraph/core/_constants.py b/src/msgraph_core/_constants.py similarity index 80% rename from msgraph/core/_constants.py rename to src/msgraph_core/_constants.py index 8cad1624..dcbdcc6e 100644 --- a/msgraph/core/_constants.py +++ b/src/msgraph_core/_constants.py @@ -8,4 +8,5 @@ """ DEFAULT_REQUEST_TIMEOUT = 100 DEFAULT_CONNECTION_TIMEOUT = 30 -SDK_VERSION = '0.2.2' +SDK_VERSION = '1.0.0' +MS_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default' diff --git a/msgraph/core/_enums.py b/src/msgraph_core/_enums.py similarity index 100% rename from msgraph/core/_enums.py rename to src/msgraph_core/_enums.py diff --git a/src/msgraph_core/authentication/__init__.py b/src/msgraph_core/authentication/__init__.py new file mode 100644 index 00000000..dfdfa7b2 --- /dev/null +++ b/src/msgraph_core/authentication/__init__.py @@ -0,0 +1,3 @@ +from .azure_identity_authentication_provider import AzureIdentityAuthenticationProvider + +__all__ = ['AzureIdentityAuthenticationProvider'] diff --git a/src/msgraph_core/authentication/azure_identity_authentication_provider.py b/src/msgraph_core/authentication/azure_identity_authentication_provider.py new file mode 100644 index 00000000..cffa36b0 --- /dev/null +++ b/src/msgraph_core/authentication/azure_identity_authentication_provider.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +from kiota_authentication_azure.azure_identity_authentication_provider import ( + AzureIdentityAuthenticationProvider as KiotaAzureIdentityAuthenticationProvider, +) + +from msgraph_core._constants import MS_DEFAULT_SCOPE +from msgraph_core._enums import NationalClouds + +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential + from azure.core.credentials_async import AsyncTokenCredential + + +class AzureIdentityAuthenticationProvider(KiotaAzureIdentityAuthenticationProvider): + + def __init__( + self, + credentials: Union["TokenCredential", "AsyncTokenCredential"], + options: Optional[Dict] = {}, + scopes: List[str] = [], + allowed_hosts: Optional[List[str]] = [nc.value for nc in NationalClouds] + ) -> None: + """[summary] + + Args: + credentials (Union["TokenCredential", "AsyncTokenCredential"]): The + tokenCredential implementation to use for authentication. + options (Optional[dict]): The options to use for authentication. + scopes (List[str]): The scopes to use for authentication. + Defaults to 'https:///.default'. + allowed_hosts (Optional[List[str]]): The allowed hosts to use for + authentication. Defaults to Microsoft National Clouds. + """ + super().__init__(credentials, options, scopes, allowed_hosts) diff --git a/src/msgraph_core/base_graph_request_adapter.py b/src/msgraph_core/base_graph_request_adapter.py new file mode 100644 index 00000000..2acfdc10 --- /dev/null +++ b/src/msgraph_core/base_graph_request_adapter.py @@ -0,0 +1,29 @@ +import httpx +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.serialization import ( + ParseNodeFactory, + ParseNodeFactoryRegistry, + SerializationWriterFactory, + SerializationWriterFactoryRegistry, +) +from kiota_http.httpx_request_adapter import HttpxRequestAdapter + +from .graph_client_factory import GraphClientFactory + + +class BaseGraphRequestAdapter(HttpxRequestAdapter): + + def __init__( + self, + authentication_provider: AuthenticationProvider, + parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), + serialization_writer_factory: + SerializationWriterFactory = SerializationWriterFactoryRegistry(), + http_client: httpx.AsyncClient = GraphClientFactory.create_with_default_middleware() + ) -> None: + super().__init__( + authentication_provider=authentication_provider, + parse_node_factory=parse_node_factory, + serialization_writer_factory=serialization_writer_factory, + http_client=http_client + ) diff --git a/src/msgraph_core/graph_client_factory.py b/src/msgraph_core/graph_client_factory.py new file mode 100644 index 00000000..bd821f3b --- /dev/null +++ b/src/msgraph_core/graph_client_factory.py @@ -0,0 +1,126 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from __future__ import annotations + +from typing import Dict, List, Optional + +import httpx +from kiota_abstractions.request_option import RequestOption +from kiota_http.kiota_client_factory import KiotaClientFactory +from kiota_http.middleware.middleware import BaseMiddleware + +from ._enums import APIVersion, NationalClouds +from .middleware import AsyncGraphTransport, GraphTelemetryHandler +from .middleware.options import GraphTelemetryHandlerOption + + +class GraphClientFactory(KiotaClientFactory): + """Constructs httpx.AsyncClient instances configured with either custom or default + pipeline of graph specific middleware. + """ + + @staticmethod + def create_with_default_middleware( + api_version: APIVersion = APIVersion.v1, + client: httpx.AsyncClient = KiotaClientFactory.get_default_client(), + host: NationalClouds = NationalClouds.Global, + options: Optional[Dict[str, RequestOption]] = None + ) -> httpx.AsyncClient: + """Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with + a custom transport loaded with a default pipeline of middleware. + + Args: + api_version (APIVersion): The Graph API version to be used. + Defaults to APIVersion.v1. + client (httpx.AsyncClient): The httpx.AsyncClient instance to be used. + Defaults to KiotaClientFactory.get_default_client(). + host (NationalClouds): The national clound endpoint to be used. + Defaults to NationalClouds.Global. + options (Optional[Dict[str, RequestOption]]): The request options to use when + instantiating default middleware. Defaults to Dict[str, RequestOption]=None. + + Returns: + httpx.AsyncClient: An instance of the AsyncClient object + """ + client.base_url = GraphClientFactory._get_base_url(host, api_version) # type: ignore + middleware = KiotaClientFactory.get_default_middleware(options) + telemetry_handler = GraphClientFactory._get_telemetry_handler(options) + middleware.append(telemetry_handler) + return GraphClientFactory._load_middleware_to_client(client, middleware) + + @staticmethod + def create_with_custom_middleware( + middleware: Optional[List[BaseMiddleware]], + api_version: APIVersion = APIVersion.v1, + client: httpx.AsyncClient = KiotaClientFactory.get_default_client(), + host: NationalClouds = NationalClouds.Global, + ) -> httpx.AsyncClient: + """Applies a custom middleware chain to the HTTP Client + + Args: + middleware(List[BaseMiddleware]): Custom middleware list that will be used to create + a middleware pipeline. The middleware should be arranged in the order in which they will + modify the request. + api_version (APIVersion): The Graph API version to be used. + Defaults to APIVersion.v1. + client (httpx.AsyncClient): The httpx.AsyncClient instance to be used. + Defaults to KiotaClientFactory.get_default_client(). + host (NationalClouds): The national clound endpoint to be used. + Defaults to NationalClouds.Global. + """ + client.base_url = GraphClientFactory._get_base_url(host, api_version) # type: ignore + return GraphClientFactory._load_middleware_to_client(client, middleware) + + @staticmethod + def _get_base_url(host: str, api_version: APIVersion) -> str: + """Helper method to set the complete base url""" + base_url = f'{host}/{api_version}' + return base_url + + @staticmethod + def _get_telemetry_handler( + options: Optional[Dict[str, RequestOption]] + ) -> GraphTelemetryHandler: + """Helper method to get the graph telemetry handler instantiated with appropriate + options""" + + if options: + graph_telemetry_options = options.get(GraphTelemetryHandlerOption().get_key()) + if graph_telemetry_options: + return GraphTelemetryHandler(options=graph_telemetry_options) + return GraphTelemetryHandler() + + @staticmethod + def _load_middleware_to_client( + client: httpx.AsyncClient, middleware: Optional[List[BaseMiddleware]] + ) -> httpx.AsyncClient: + current_transport = client._transport + client._transport = GraphClientFactory._replace_transport_with_custom_graph_transport( + current_transport, middleware + ) + if client._mounts: + mounts: dict = {} + for pattern, transport in client._mounts.items(): + if transport is None: + mounts[pattern] = None + else: + mounts[pattern + ] = GraphClientFactory._replace_transport_with_custom_graph_transport( + transport, middleware + ) + client._mounts = dict(sorted(mounts.items())) + return client + + @staticmethod + def _replace_transport_with_custom_graph_transport( + current_transport: httpx.AsyncBaseTransport, middleware: Optional[List[BaseMiddleware]] + ) -> AsyncGraphTransport: + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( + middleware, current_transport + ) + new_transport = AsyncGraphTransport( + transport=current_transport, pipeline=middleware_pipeline + ) + return new_transport diff --git a/tests/integration/__init__.py b/src/msgraph_core/middleware/__init__.py similarity index 50% rename from tests/integration/__init__.py rename to src/msgraph_core/middleware/__init__.py index b74cfa3b..1ecb083a 100644 --- a/tests/integration/__init__.py +++ b/src/msgraph_core/middleware/__init__.py @@ -2,3 +2,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from .async_graph_transport import AsyncGraphTransport +from .request_context import GraphRequestContext +from .telemetry import GraphTelemetryHandler diff --git a/src/msgraph_core/middleware/async_graph_transport.py b/src/msgraph_core/middleware/async_graph_transport.py new file mode 100644 index 00000000..bb81ff69 --- /dev/null +++ b/src/msgraph_core/middleware/async_graph_transport.py @@ -0,0 +1,41 @@ +import json + +import httpx +from kiota_http.middleware import MiddlewarePipeline, RedirectHandler, RetryHandler + +from .._enums import FeatureUsageFlag +from .request_context import GraphRequestContext + + +class AsyncGraphTransport(httpx.AsyncBaseTransport): + """A custom transport for requests to the Microsoft Graph API + """ + + def __init__(self, transport: httpx.AsyncBaseTransport, pipeline: MiddlewarePipeline) -> None: + self.transport = transport + self.pipeline = pipeline + + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + if self.pipeline and hasattr(request, 'options'): + self.set_request_context_and_feature_usage(request) + response = await self.pipeline.send(request) + return response + + response = await self.transport.handle_async_request(request) + return response + + def set_request_context_and_feature_usage(self, request: httpx.Request) -> httpx.Request: + + request_options = request.options # type:ignore + + context = GraphRequestContext(request_options, request.headers) + middleware = self.pipeline._first_middleware + while middleware: + if isinstance(middleware, RedirectHandler): + context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + if isinstance(middleware, RetryHandler): + context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED + + middleware = middleware.next + request.context = context #type: ignore + return request diff --git a/src/msgraph_core/middleware/options/__init__.py b/src/msgraph_core/middleware/options/__init__.py new file mode 100644 index 00000000..1cef83d4 --- /dev/null +++ b/src/msgraph_core/middleware/options/__init__.py @@ -0,0 +1 @@ +from .graph_telemetry_handler_option import GraphTelemetryHandlerOption diff --git a/src/msgraph_core/middleware/options/graph_telemetry_handler_option.py b/src/msgraph_core/middleware/options/graph_telemetry_handler_option.py new file mode 100644 index 00000000..28c33004 --- /dev/null +++ b/src/msgraph_core/middleware/options/graph_telemetry_handler_option.py @@ -0,0 +1,49 @@ +from typing import List, Optional + +from kiota_abstractions.request_option import RequestOption + +from ..._constants import SDK_VERSION +from ..._enums import APIVersion + + +class GraphTelemetryHandlerOption(RequestOption): + """Config options for the GraphTelemetryHandler + """ + + GRAPH_TELEMETRY_HANDLER_OPTION_KEY = "GraphTelemetryHandlerOption" + + def __init__( + self, api_version: Optional[APIVersion] = None, sdk_version: str = SDK_VERSION + ) -> None: + """To create an instance of GraphTelemetryHandlerOption + + Args: + api_version (Optional[APIVersion], optional): The Graph API version in use. + Defaults to None. + sdk_version (str): The sdk version in use. + Defaults to SDK_VERSION of grap core. + """ + self._api_version = api_version + self._sdk_version = sdk_version + + @property + def api_version(self): + """The Graph API version in use""" + return self._api_version + + @api_version.setter + def api_version(self, value: APIVersion): + self._api_version = value + + @property + def sdk_version(self): + """The sdk version in use""" + return self._sdk_version + + @sdk_version.setter + def sdk_version(self, value: str): + self._sdk_version = value + + @staticmethod + def get_key() -> str: + return GraphTelemetryHandlerOption.GRAPH_TELEMETRY_HANDLER_OPTION_KEY diff --git a/msgraph/core/middleware/request_context.py b/src/msgraph_core/middleware/request_context.py similarity index 66% rename from msgraph/core/middleware/request_context.py rename to src/msgraph_core/middleware/request_context.py index 2e825c33..1f520a15 100644 --- a/msgraph/core/middleware/request_context.py +++ b/src/msgraph_core/middleware/request_context.py @@ -4,17 +4,19 @@ # ------------------------------------ import uuid +import httpx + from .._enums import FeatureUsageFlag -class RequestContext: - """A request context contains data that is persisted throughout the request and - includes a ClientRequestId property, MiddlewareControl property to control behavior - of middleware as well as a FeatureUsage  property to keep track of middleware used +class GraphRequestContext: + """A request context contains data that is persisted throughout the request and + includes a ClientRequestId property, MiddlewareControl property to control behavior + of middleware as well as a FeatureUsage property to keep track of middleware used in making the request. """ - def __init__(self, middleware_control, headers): + def __init__(self, middleware_control: dict, headers: httpx.Headers): """Constructor for request context instances Args: @@ -27,12 +29,12 @@ def __init__(self, middleware_control, headers): """ self.middleware_control = middleware_control self.client_request_id = headers.get('client-request-id', str(uuid.uuid4())) - self._feature_usage = FeatureUsageFlag.NONE + self._feature_usage: int = FeatureUsageFlag.NONE @property def feature_usage(self): return hex(self._feature_usage) @feature_usage.setter - def set_feature_usage(self, flag: FeatureUsageFlag): + def feature_usage(self, flag: FeatureUsageFlag) -> None: self._feature_usage = self._feature_usage | flag diff --git a/src/msgraph_core/middleware/telemetry.py b/src/msgraph_core/middleware/telemetry.py new file mode 100644 index 00000000..1f6a604b --- /dev/null +++ b/src/msgraph_core/middleware/telemetry.py @@ -0,0 +1,128 @@ +import http +import json +import platform + +import httpx +from kiota_http.middleware import BaseMiddleware +from urllib3.util import parse_url + +from .._constants import SDK_VERSION +from .._enums import APIVersion, NationalClouds +from .async_graph_transport import AsyncGraphTransport +from .options import GraphTelemetryHandlerOption +from .request_context import GraphRequestContext + + +class GraphRequest(httpx.Request): + context: GraphRequestContext + + +class GraphTelemetryHandler(BaseMiddleware): + """Middleware component that attaches metadata to a Graph request in order to help + the SDK team improve the developer experience. + """ + + def __init__( + self, options: GraphTelemetryHandlerOption = GraphTelemetryHandlerOption(), **kwargs + ): + """Create an instance of GraphTelemetryHandler + + Args: + options (GraphTelemetryHandlerOption, optional): The graph telemetry handler + options value. Defaults to GraphTelemetryHandlerOption + """ + super().__init__() + self.options = options + + async def send(self, request: GraphRequest, transport: AsyncGraphTransport): + """Adds telemetry headers and sends the http request. + """ + current_options = self._get_current_options(request) + + if self.is_graph_url(request.url): + self._add_client_request_id_header(request) + self._append_sdk_version_header(request, current_options) + self._add_host_os_header(request) + self._add_runtime_environment_header(request) + + response = await super().send(request, transport) + return response + + def _get_current_options(self, request: httpx.Request) -> GraphTelemetryHandlerOption: + """Returns the options to use for the request.Overries default options if + request options are passed. + + Args: + request (httpx.Request): The prepared request object + + Returns: + GraphTelemetryHandlerOption: The options to used. + """ + current_options = self.options + request_options = request.context.middleware_control.get( # type:ignore + GraphTelemetryHandlerOption.get_key() + ) + # Override default options with request options + if request_options: + current_options = request_options + + return current_options + + def is_graph_url(self, url): + """Check if the request is made to a graph endpoint. We do not add telemetry headers to + non-graph endpoints""" + endpoints = set(item.value for item in NationalClouds) + + base_url = parse_url(str(url)) + endpoint = f"{base_url.scheme}://{base_url.netloc}" + return endpoint in endpoints + + def _add_client_request_id_header(self, request) -> None: + """Add a client-request-id header with GUID value to request""" + request.headers.update({'client-request-id': f'{request.context.client_request_id}'}) + + def _append_sdk_version_header(self, request, options) -> None: + """Add SdkVersion request header to each request to identify the language and + version of the client SDK library(s). + Also adds the featureUsage value. + """ + core_library_name = f'graph-python-core/{SDK_VERSION}' + service_lib_name = '' + + if options.api_version == APIVersion.v1: + service_lib_name = f'graph-python/{options.sdk_version}' + if options.api_version == APIVersion.beta: + service_lib_name = f'graph-python-beta/{options.sdk_version}' + + if service_lib_name: + telemetry_header_string = f'{service_lib_name}, '\ + f'{core_library_name} (featureUsage={request.context.feature_usage})' + else: + telemetry_header_string = f'{core_library_name} '\ + '(featureUsage={request.context.feature_usage})' + + if 'sdkVersion' in request.headers: + sdk_version = request.headers.get('sdkVersion') + if not sdk_version == telemetry_header_string: + request.headers.update({'sdkVersion': telemetry_header_string}) + else: + request.headers.update({'sdkVersion': telemetry_header_string}) + + def _add_host_os_header(self, request) -> None: + """ + Add HostOS request header to each request to help identify the OS + on which our client SDK is running on + """ + system = platform.system() + version = platform.version() + host_os = f'{system} {version}' + request.headers.update({'HostOs': host_os}) + + def _add_runtime_environment_header(self, request) -> None: + """ + Add RuntimeEnvironment request header to capture the runtime framework + on which the client SDK is running on. + """ + python_version = platform.python_version() + runtime_environment = f'Python/{python_version}' + request.headers.update({'RuntimeEnvironment': runtime_environment}) diff --git a/tests/authentication/test_azure_identity_authentication_provider.py b/tests/authentication/test_azure_identity_authentication_provider.py new file mode 100644 index 00000000..a45decb8 --- /dev/null +++ b/tests/authentication/test_azure_identity_authentication_provider.py @@ -0,0 +1,8 @@ +from azure.identity import EnvironmentCredential +from kiota_abstractions.authentication import AuthenticationProvider + +from msgraph_core.authentication import AzureIdentityAuthenticationProvider + + +def test_subclassing(): + assert issubclass(AzureIdentityAuthenticationProvider, AuthenticationProvider) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..9703b820 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,52 @@ +import httpx +import pytest +from kiota_abstractions.authentication import AnonymousAuthenticationProvider + +from msgraph_core import APIVersion, NationalClouds +from msgraph_core.graph_client_factory import GraphClientFactory +from msgraph_core.middleware import GraphRequestContext + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +class MockAuthenticationProvider(AnonymousAuthenticationProvider): + + async def get_authorization_token(self, request: httpx.Request) -> str: + """Returns a string representing a dummy token + Args: + request (GraphRequest): Graph request object + """ + request.headers['Authorization'] = 'Sample token' + return + + +@pytest.fixture +def mock_auth_provider(): + return MockAuthenticationProvider() + + +@pytest.fixture +def mock_transport(): + client = GraphClientFactory.create_with_default_middleware() + return client._transport + + +@pytest.fixture +def mock_request(): + req = httpx.Request('GET', "https://example.org") + req.options = {} + return req + + +@pytest.fixture +def mock_graph_request(): + req = httpx.Request('GET', BASE_URL) + req.context = GraphRequestContext({}, req.headers) + return req + + +@pytest.fixture +def mock_response(): + return httpx.Response( + json={'message': 'Success!'}, status_code=200, headers={"Content-Type": "application/json"} + ) diff --git a/tests/integration/test_graphclient.py b/tests/integration/test_graphclient.py deleted file mode 100644 index 3772a9d7..00000000 --- a/tests/integration/test_graphclient.py +++ /dev/null @@ -1,91 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential -from requests import Session - -from msgraph.core import APIVersion, GraphClient -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_graph_client_with_default_middleware(): - """ - Test that a graph client uses default middleware if none are provided - """ - # credential = _CustomTokenCredential() - client = GraphClient(credential=EnvironmentCredential()) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_graph_client_with_user_provided_session(): - """ - Test that the graph client works with a user provided session object - """ - - session = Session() - client = GraphClient(session=session, credential=EnvironmentCredential()) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_with_custom_settings(): - """ - Test that the graph client works with user provided configuration - """ - credential = EnvironmentCredential() - client = GraphClient(api_version=APIVersion.beta, credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_with_custom_middleware(): - """ - Test client factory works with user provided middleware - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = GraphClient(middleware=middleware) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_adds_context_to_request(): - """ - Test the graph client adds a context object to a request - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes) - assert response.status_code == 200 - assert hasattr(response.request, 'context') - - -def test_graph_client_picks_options_from_kwargs(): - """ - Test the graph client picks middleware options from kwargs and sets them in the context - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes) - assert response.status_code == 200 - assert 'scopes' in response.request.context.middleware_control.keys() - assert response.request.context.middleware_control['scopes'] == scopes - - -def test_graph_client_allows_passing_optional_kwargs(): - """ - Test the graph client allows passing optional kwargs native to the requests library - such as stream, proxy and cert. - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes, stream=True) - assert response.status_code == 200 diff --git a/tests/integration/test_http_client_factory.py b/tests/integration/test_http_client_factory.py deleted file mode 100644 index ea4cbb0e..00000000 --- a/tests/integration/test_http_client_factory.py +++ /dev/null @@ -1,86 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential -from requests import Session - -from msgraph.core import APIVersion, HTTPClientFactory -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_client_factory_with_default_middleware(): - """ - Test that a client created from client factory with default middleware - works as expected. - """ - credential = EnvironmentCredential() - client = HTTPClientFactory().create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_user_provided_session(): - """ - Test that the client works with a user provided session object - """ - - session = Session() - credential = EnvironmentCredential() - client = HTTPClientFactory(session=session).create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_custom_settings(): - """ - Test that the client works with user provided configuration - """ - credential = EnvironmentCredential() - client = HTTPClientFactory(api_version=APIVersion.beta - ).create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_custom_middleware(): - """ - Test client factory works with user provided middleware - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_context_object_is_attached_to_requests_from_client_factory(): - """ - Test that requests from a native HTTP client have a context object attached - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - assert hasattr(response.request, 'context') - - -def test_middleware_control_is_empty_for_requests_from_client_factory(): - """ - Test that requests from a native HTTP client have no middlware options in the middleware - control - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - assert response.request.context.middleware_control == {} diff --git a/tests/integration/test_retry.py b/tests/integration/test_retry.py deleted file mode 100644 index af0f8322..00000000 --- a/tests/integration/test_retry.py +++ /dev/null @@ -1,91 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential - -from msgraph.core import GraphClient - - -@pytest.fixture -def graph_client(): - scopes = ['https://graph.microsoft.com/.default'] - credential = EnvironmentCredential() - client = GraphClient(credential=credential, scopes=scopes) - return client - - -def test_no_retry_success_response(graph_client): - """ - Test that a request with valid http header and a success response is not retried - """ - response = graph_client.get('https://graph.microsoft.com/v1.0/users') - - assert response.status_code == 200 - with pytest.raises(KeyError): - response.request.headers["retry-attempt"] - - -def test_valid_retry_429(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/429') - - assert response.status_code == 429 - assert response.request.headers["retry-attempt"] == "3" - - -def test_valid_retry_503(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/503') - - assert response.status_code == 503 - assert response.request.headers["retry-attempt"] == "3" - - -def test_valid_retry_504(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/504') - - assert response.status_code == 504 - assert response.request.headers["retry-attempt"] == "3" - - -def test_request_specific_options_override_default(graph_client): - """ - Test that retry options passed to the request take precedence over - the default options. - """ - response_1 = graph_client.get('https://httpbin.org/status/429') - response_2 = graph_client.get('https://httpbin.org/status/503', max_retries=2) - response_3 = graph_client.get('https://httpbin.org/status/504') - response_4 = graph_client.get('https://httpbin.org/status/429', max_retries=1) - - assert response_1.status_code == 429 - assert response_1.request.headers["Retry-Attempt"] == "3" - assert response_2.status_code == 503 - assert response_2.request.headers["Retry-Attempt"] == "2" - assert response_3.status_code == 504 - assert response_3.request.headers["Retry-Attempt"] == "3" - assert response_4.status_code == 429 - assert response_4.request.headers["Retry-Attempt"] == "1" - - -def test_retries_time_limit(graph_client): - """ - Test that the cumulative retry time plus the retry-after values does not exceed the - provided retries time limit - """ - - response = graph_client.get('https://httpbin.org/status/503', retry_time_limit=0.1) - - assert response.status_code == 503 - headers = response.request.headers - with pytest.raises(KeyError): - response.request.headers["retry-attempt"] diff --git a/tests/integration/test_telemetry.py b/tests/integration/test_telemetry.py deleted file mode 100644 index 6346ecef..00000000 --- a/tests/integration/test_telemetry.py +++ /dev/null @@ -1,72 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import platform -import re -import uuid - -import pytest -from azure.identity import EnvironmentCredential - -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -@pytest.fixture -def graph_client(): - scopes = ['https://graph.microsoft.com/.default'] - credential = EnvironmentCredential() - client = GraphClient(credential=credential, scopes=scopes) - return client - - -def test_telemetry_handler(graph_client): - """ - Test telemetry handler updates the graph request with the requisite headers - """ - response = graph_client.get('https://graph.microsoft.com/v1.0/users') - system = platform.system() - version = platform.version() - host_os = f'{system} {version}' - python_version = platform.python_version() - runtime_environment = f'Python/{python_version}' - - assert response.status_code == 200 - assert response.request.headers["client-request-id"] - assert response.request.headers["sdkVersion"].startswith('graph-python-core/' + SDK_VERSION) - assert response.request.headers["HostOs"] == host_os - assert response.request.headers["RuntimeEnvironment"] == runtime_environment - - -def test_telemetry_handler_non_graph_url(graph_client): - """ - Test telemetry handler does not updates the request headers for non-graph requests - """ - response = graph_client.get('https://httpbin.org/status/200') - - assert response.status_code == 200 - with pytest.raises(KeyError): - response.request.headers["client-request-id"] - response.request.headers["sdkVersion"] - response.request.headers["HostOs"] - response.request.headers["RuntimeEnvironment"] - - -def test_custom_client_request_id(graph_client): - """ - Test customer provided client request id overrides default value - """ - custom_id = str(uuid.uuid4()) - response = graph_client.get( - 'https://httpbin.org/status/200', headers={"client-request-id": custom_id} - ) - - assert response.status_code == 200 - assert response.request.context.client_request_id == custom_id - with pytest.raises(KeyError): - response.request.headers["client-request-id"] - response.request.headers["sdkVersion"] - response.request.headers["HostOs"] - response.request.headers["RuntimeEnvironment"] diff --git a/tests/middleware/options/test_graph_telemetry_handler_options.py b/tests/middleware/options/test_graph_telemetry_handler_options.py new file mode 100644 index 00000000..7725507f --- /dev/null +++ b/tests/middleware/options/test_graph_telemetry_handler_options.py @@ -0,0 +1,23 @@ +import pytest + +from src.msgraph_core._constants import SDK_VERSION +from src.msgraph_core._enums import APIVersion +from src.msgraph_core.middleware.options import GraphTelemetryHandlerOption + + +def test_graph_telemetry_handler_options_default(): + telemetry_options = GraphTelemetryHandlerOption() + + assert telemetry_options.get_key() == "GraphTelemetryHandlerOption" + assert telemetry_options.api_version is None + assert telemetry_options.sdk_version == SDK_VERSION + + +def test_graph_telemetry_handler_options_custom(): + telemetry_options = GraphTelemetryHandlerOption( + api_version=APIVersion.beta, sdk_version='1.0.0' + ) + + assert telemetry_options.get_key() == "GraphTelemetryHandlerOption" + assert telemetry_options.api_version == APIVersion.beta + assert telemetry_options.sdk_version == '1.0.0' diff --git a/tests/middleware/test_async_graph_transport.py b/tests/middleware/test_async_graph_transport.py new file mode 100644 index 00000000..38e1bca7 --- /dev/null +++ b/tests/middleware/test_async_graph_transport.py @@ -0,0 +1,18 @@ +import pytest +from kiota_http.kiota_client_factory import KiotaClientFactory + +from msgraph_core._enums import FeatureUsageFlag +from msgraph_core.middleware import AsyncGraphTransport, GraphRequestContext + + +def test_set_request_context_and_feature_usage(mock_request, mock_transport): + middleware = KiotaClientFactory.get_default_middleware(None) + pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, mock_transport) + transport = AsyncGraphTransport(mock_transport, pipeline) + transport.set_request_context_and_feature_usage(mock_request) + + assert hasattr(mock_request, 'context') + assert isinstance(mock_request.context, GraphRequestContext) + assert mock_request.context.feature_usage == hex( + FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + ) diff --git a/tests/middleware/test_graph_telemetry_handler.py b/tests/middleware/test_graph_telemetry_handler.py new file mode 100644 index 00000000..53a36698 --- /dev/null +++ b/tests/middleware/test_graph_telemetry_handler.py @@ -0,0 +1,123 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import platform +import re +import uuid + +import httpx +import pytest + +from msgraph_core import SDK_VERSION, APIVersion, NationalClouds +from msgraph_core.middleware import GraphRequestContext, GraphTelemetryHandler +from msgraph_core.middleware.options import GraphTelemetryHandlerOption + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +def test_is_graph_url(mock_graph_request): + """ + Test method that checks whether a request url is a graph endpoint + """ + telemetry_handler = GraphTelemetryHandler() + assert telemetry_handler.is_graph_url(mock_graph_request.url) + + +def test_is_not_graph_url(mock_request): + """ + Test method that checks whether a request url is a graph endpoint with a + non-graph url. + """ + telemetry_handler = GraphTelemetryHandler() + assert not telemetry_handler.is_graph_url(mock_request.url) + + +def test_add_client_request_id_header(mock_graph_request): + """ + Test that client_request_id is added to the request headers + """ + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_client_request_id_header(mock_graph_request) + + assert 'client-request-id' in mock_graph_request.headers + assert _is_valid_uuid(mock_graph_request.headers.get('client-request-id')) + + +def test_custom_client_request_id_header(): + """ + Test that a custom client request id is used, if provided + """ + custom_id = str(uuid.uuid4()) + request = httpx.Request('GET', BASE_URL) + request.context = GraphRequestContext({}, {'client-request-id': custom_id}) + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_client_request_id_header(request) + + assert 'client-request-id' in request.headers + assert _is_valid_uuid(request.headers.get('client-request-id')) + assert request.headers.get('client-request-id') == custom_id + + +def test_append_sdk_version_header(mock_graph_request): + """ + Test that sdkVersion is added to the request headers + """ + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._append_sdk_version_header(mock_graph_request, telemetry_handler.options) + + assert 'sdkVersion' in mock_graph_request.headers + assert mock_graph_request.headers.get('sdkVersion' + ).startswith('graph-python-core/' + SDK_VERSION) + + +def test_append_sdk_version_header_beta(mock_graph_request): + """ + Test that sdkVersion is added to the request headers + """ + telemetry_options = GraphTelemetryHandlerOption( + api_version=APIVersion.beta, sdk_version='1.0.0' + ) + telemetry_handler = GraphTelemetryHandler(options=telemetry_options) + telemetry_handler._append_sdk_version_header(mock_graph_request, telemetry_options) + + assert 'sdkVersion' in mock_graph_request.headers + assert mock_graph_request.headers.get('sdkVersion').startswith('graph-python-beta/' + '1.0.0') + + +def test_add_host_os_header(mock_graph_request): + """ + Test that HostOs is added to the request headers + """ + system = platform.system() + version = platform.version() + host_os = f'{system} {version}' + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_host_os_header(mock_graph_request) + + assert 'HostOs' in mock_graph_request.headers + assert mock_graph_request.headers.get('HostOs') == host_os + + +def test_add_runtime_environment_header(mock_graph_request): + """ + Test that RuntimeEnvironment is added to the request headers + """ + python_version = platform.python_version() + runtime_environment = f'Python/{python_version}' + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_runtime_environment_header(mock_graph_request) + + assert 'RuntimeEnvironment' in mock_graph_request.headers + assert mock_graph_request.headers.get('RuntimeEnvironment') == runtime_environment + + +def _is_valid_uuid(guid): + regex = "^[{]?[0-9a-fA-F]{8}" + "-([0-9a-fA-F]{4}-)" + "{3}[0-9a-fA-F]{12}[}]?$" + pattern = re.compile(regex) + if re.search(pattern, guid): + return True + return False diff --git a/tests/test_base_graph_request_adapter.py b/tests/test_base_graph_request_adapter.py new file mode 100644 index 00000000..59e7c891 --- /dev/null +++ b/tests/test_base_graph_request_adapter.py @@ -0,0 +1,24 @@ +import httpx +import pytest +from kiota_abstractions.serialization import ( + ParseNodeFactoryRegistry, + SerializationWriterFactoryRegistry, +) + +from msgraph_core.base_graph_request_adapter import BaseGraphRequestAdapter + + +def test_create_graph_request_adapter(mock_auth_provider): + request_adapter = BaseGraphRequestAdapter(mock_auth_provider) + assert request_adapter._authentication_provider is mock_auth_provider + assert isinstance(request_adapter._parse_node_factory, ParseNodeFactoryRegistry) + assert isinstance( + request_adapter._serialization_writer_factory, SerializationWriterFactoryRegistry + ) + assert isinstance(request_adapter._http_client, httpx.AsyncClient) + assert request_adapter.base_url == '' + + +def test_create_request_adapter_no_auth_provider(): + with pytest.raises(TypeError): + BaseGraphRequestAdapter(None) diff --git a/tests/test_graph_client_factory.py b/tests/test_graph_client_factory.py new file mode 100644 index 00000000..36138555 --- /dev/null +++ b/tests/test_graph_client_factory.py @@ -0,0 +1,145 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import httpx +import pytest +from kiota_http.middleware import MiddlewarePipeline, RedirectHandler, RetryHandler +from kiota_http.middleware.options import RedirectHandlerOption, RetryHandlerOption + +from msgraph_core import APIVersion, GraphClientFactory, NationalClouds +from msgraph_core.middleware import AsyncGraphTransport, GraphTelemetryHandler + + +def test_create_with_default_middleware(): + """Test creation of GraphClient using default middleware""" + client = GraphClientFactory.create_with_default_middleware() + + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) + + +def test_create_with_default_middleware_custom_client(): + """Test creation of GraphClient using default middleware""" + timeout = httpx.Timeout(20, connect=10) + custom_client = httpx.AsyncClient(timeout=timeout, http2=True) + client = GraphClientFactory.create_with_default_middleware(client=custom_client) + + assert isinstance(client, httpx.AsyncClient) + assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) + + +def test_create_with_default_middleware_custom_client_with_proxy(): + """Test creation of GraphClient using default middleware""" + proxies = { + "http://": "http://localhost:8030", + "https://": "http://localhost:8031", + } + timeout = httpx.Timeout(20, connect=10) + custom_client = httpx.AsyncClient(timeout=timeout, http2=True, proxies=proxies) + client = GraphClientFactory.create_with_default_middleware(client=custom_client) + + assert isinstance(client, httpx.AsyncClient) + assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) + assert client._mounts + for pattern, transport in client._mounts.items(): + assert isinstance(transport, AsyncGraphTransport) + + +def test_create_default_with_custom_middleware(): + """Test creation of HTTP Client using default middleware and custom options""" + retry_options = RetryHandlerOption(max_retries=5) + options = {f'{retry_options.get_key()}': retry_options} + client = GraphClientFactory.create_with_default_middleware(options=options) + + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + retry_handler = pipeline._first_middleware.next + assert isinstance(retry_handler, RetryHandler) + assert retry_handler.options.max_retry == retry_options.max_retry + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) + + +def test_create_with_custom_middleware_custom_client(): + """Test creation of HTTP Clients with custom middleware""" + timeout = httpx.Timeout(20, connect=10) + custom_client = httpx.AsyncClient(timeout=timeout, http2=True) + middleware = [ + GraphTelemetryHandler(), + ] + client = GraphClientFactory.create_with_custom_middleware( + middleware=middleware, client=custom_client + ) + + assert isinstance(client, httpx.AsyncClient) + assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) + + +def test_create_with_custom_middleware_custom_client_with_proxy(): + """Test creation of HTTP Clients with custom middleware""" + proxies = { + "http://": "http://localhost:8030", + "https://": "http://localhost:8031", + } + timeout = httpx.Timeout(20, connect=10) + custom_client = httpx.AsyncClient(timeout=timeout, http2=True, proxies=proxies) + middleware = [ + GraphTelemetryHandler(), + ] + client = GraphClientFactory.create_with_custom_middleware( + middleware=middleware, client=custom_client + ) + + assert isinstance(client, httpx.AsyncClient) + assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) + assert isinstance(client._transport, AsyncGraphTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) + assert client._mounts + for pattern, transport in client._mounts.items(): + assert isinstance(transport, AsyncGraphTransport) + pipeline = transport.pipeline + assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) + + +def test_graph_client_factory_with_custom_configuration(): + """ + Test creating a graph client with custom url overrides the default + """ + graph_client = GraphClientFactory.create_with_default_middleware( + api_version=APIVersion.beta, host=NationalClouds.China + ) + assert isinstance(graph_client, httpx.AsyncClient) + assert str(graph_client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' + + +def test_get_base_url(): + """ + Test base url is formed by combining the national cloud endpoint with + Api version + """ + url = GraphClientFactory._get_base_url( + host=NationalClouds.Germany, + api_version=APIVersion.beta, + ) + assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index b74cfa3b..00000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ diff --git a/tests/unit/test_auth_handler.py b/tests/unit/test_auth_handler.py deleted file mode 100644 index 50b40dcd..00000000 --- a/tests/unit/test_auth_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest - -from msgraph.core.middleware.authorization import AuthorizationHandler -from msgraph.core.middleware.request_context import RequestContext - - -def test_context_options_override_default_scopes(): - """ Test scopes found in the request context override default scopes""" - default_scopes = ['.default'] - middleware_control = { - 'scopes': ['email.read'], - } - request_context = RequestContext(middleware_control, {}) - - auth_handler = AuthorizationHandler(None, scopes=default_scopes) - - auth_handler_scopes = auth_handler.get_scopes(request_context) - assert auth_handler_scopes == middleware_control['scopes'] - - -def test_auth_handler_get_scopes_does_not_overwrite_default_scopes(): - default_scopes = ['.default'] - middleware_control = { - 'scopes': ['email.read'], - } - request_context = RequestContext(middleware_control, {}) - auth_handler = AuthorizationHandler(None, scopes=default_scopes) - - auth_handler_scopes = auth_handler.get_scopes(request_context) - - assert auth_handler.scopes == default_scopes diff --git a/tests/unit/test_client_factory.py b/tests/unit/test_client_factory.py deleted file mode 100644 index 80f6a571..00000000 --- a/tests/unit/test_client_factory.py +++ /dev/null @@ -1,79 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from requests import Session -from requests.adapters import HTTPAdapter - -from msgraph.core import APIVersion, HTTPClientFactory, NationalClouds -from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_initialize_with_default_config(): - """Test creation of HTTP Client will use the default configuration - if none are passed""" - client = HTTPClientFactory() - - assert client.api_version == APIVersion.v1 - assert client.endpoint == NationalClouds.Global - assert client.timeout == (DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT) - assert isinstance(client.session, Session) - - -def test_initialize_with_custom_config(): - """Test creation of HTTP Client will use custom configuration if they are passed""" - client = HTTPClientFactory(api_version=APIVersion.beta, timeout=(5, 5)) - - assert client.api_version == APIVersion.beta - assert client.endpoint == NationalClouds.Global - assert client.timeout == (5, 5) - assert isinstance(client.session, Session) - - -def test_create_with_default_middleware(): - """Test creation of HTTP Client using default middleware""" - credential = _CustomTokenCredential() - client = HTTPClientFactory().create_with_default_middleware(credential=credential) - middleware = client.get_adapter('https://') - - assert isinstance(middleware, HTTPAdapter) - - -def test_create_with_custom_middleware(): - """Test creation of HTTP Clients with custom middleware""" - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware=middleware) - custom_middleware = client.get_adapter('https://') - - assert isinstance(custom_middleware, HTTPAdapter) - - -def test_get_base_url(): - """ - Test base url is formed by combining the national cloud endpoint with - Api version - """ - client = HTTPClientFactory(api_version=APIVersion.beta, cloud=NationalClouds.Germany) - assert client.session.base_url == client.endpoint + '/' + client.api_version - - -def test_register_middleware(): - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory() - client._register(middleware) - - assert isinstance(client.session.get_adapter('https://'), HTTPAdapter) - - -class _CustomTokenCredential: - - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py deleted file mode 100644 index 02b6d624..00000000 --- a/tests/unit/test_graph_client.py +++ /dev/null @@ -1,97 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -import responses -from requests import Session -from requests.adapters import HTTPAdapter - -from msgraph.core import APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_graph_client_with_default_middleware(): - """ - Test creating a graph client with default middleware works as expected - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - - assert isinstance(client.graph_session, Session) - assert isinstance(client.graph_session.get_adapter('https://'), HTTPAdapter) - assert client.graph_session.base_url == NationalClouds.Global + '/' + APIVersion.v1 - - -def test_graph_client_with_custom_middleware(): - """ - Test creating a graph client with custom middleware works as expected - """ - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = GraphClient(middleware=middleware) - - assert isinstance(client.graph_session, Session) - assert isinstance(client.graph_session.get_adapter('https://'), HTTPAdapter) - assert client.graph_session.base_url == NationalClouds.Global + '/' + APIVersion.v1 - - -def test_graph_client_with_custom_configuration(): - """ - Test creating a graph client with custom middleware works as expected - """ - credential = _CustomTokenCredential() - client = GraphClient( - credential=credential, api_version=APIVersion.beta, cloud=NationalClouds.China - ) - - assert client.graph_session.base_url == NationalClouds.China + '/' + APIVersion.beta - - -def test_graph_client_uses_same_session(): - """ - Test graph client is a singleton class and uses the same session - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - - client2 = GraphClient(credential=credential) - assert client is client2 - - -@responses.activate -def test_graph_client_builds_graph_urls(): - """ - Test that the graph client builds full urls if supplied with partial - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - graph_url = client.graph_session.base_url + '/me' - - responses.add(responses.GET, graph_url, status=200) - - client.get('/me', headers={}) - assert graph_url == responses.calls[0].request.url - - -@responses.activate -def test_does_not_build_graph_urls_for_full_urls(): - """ - Test that the graph client builds full urls if supplied with partial - """ - other_url = 'https://microsoft.com/' - responses.add(responses.GET, other_url, status=200) - - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - client.get(other_url, headers={}) - request_url = responses.calls[0].request.url - assert other_url == request_url - - -class _CustomTokenCredential: - - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] diff --git a/tests/unit/test_middleware_pipeline.py b/tests/unit/test_middleware_pipeline.py deleted file mode 100644 index 5fcbac35..00000000 --- a/tests/unit/test_middleware_pipeline.py +++ /dev/null @@ -1,94 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from collections import OrderedDict -from unittest import TestCase - -from msgraph.core.middleware.middleware import BaseMiddleware, MiddlewarePipeline - - -class MiddlewarePipelineTest(TestCase): - - def test_adds_middlewares_in_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware(MockRequestMiddleware1()) - middleware_pipeline.add_middleware(MockRequestMiddleware2()) - - first_middleware = middleware_pipeline._first_middleware - second_middleware = middleware_pipeline._first_middleware.next - - self.assertIsInstance(first_middleware, MockRequestMiddleware1) - self.assertIsInstance(second_middleware, MockRequestMiddleware2) - - def test_request_object_is_modified_in_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware(MockRequestMiddleware1()) - middleware_pipeline.add_middleware(MockRequestMiddleware2()) - - request = OrderedDict() - request.headers = {} - result = middleware_pipeline.send(request) - - second, _ = result.popitem() - first, _ = result.popitem() - - self.assertEqual(second, 'middleware2') - self.assertEqual(first, 'middleware1') - - def test_response_object_is_modified_in_reverse_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware( - MockResponseMiddleware1() - ) # returns world as the response - middleware_pipeline.add_middleware( - MockResponseMiddleware2() - ) # returns hello as the response - - # Responses are passed through the list of middlewares in reverse order. - # This will return hello world - request = OrderedDict() - request.headers = {} - resp = middleware_pipeline.send(request) - - self.assertEqual(resp, 'Hello World') - - -class MockRequestMiddleware1(BaseMiddleware): - - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - request['middleware1'] = 1 - return super().send(request, **kwargs) - - -class MockRequestMiddleware2(BaseMiddleware): - - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - request['middleware2'] = 2 - return request - - -class MockResponseMiddleware1(BaseMiddleware): - - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - resp = super().send(request, **kwargs) - resp += 'World' - return resp - - -class MockResponseMiddleware2(BaseMiddleware): - - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - return 'Hello ' diff --git a/tests/unit/test_retry_handler.py b/tests/unit/test_retry_handler.py deleted file mode 100644 index 9201801c..00000000 --- a/tests/unit/test_retry_handler.py +++ /dev/null @@ -1,192 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from email.utils import formatdate -from time import time - -import pytest -import requests -import responses - -from msgraph.core import APIVersion, NationalClouds -from msgraph.core.middleware.retry import RetryHandler - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -def test_no_config(): - """ - Test that default values are used if no custom confguration is passed - """ - retry_handler = RetryHandler() - assert retry_handler.max_retries == retry_handler.DEFAULT_MAX_RETRIES - assert retry_handler.timeout == retry_handler.MAX_DELAY - assert retry_handler.backoff_max == retry_handler.MAXIMUM_BACKOFF - assert retry_handler.backoff_factor == retry_handler.DEFAULT_BACKOFF_FACTOR - assert retry_handler._allowed_methods == frozenset( - ['HEAD', 'GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS'] - ) - assert retry_handler._respect_retry_after_header - assert retry_handler._retry_on_status_codes == retry_handler._DEFAULT_RETRY_STATUS_CODES - - -def test_custom_config(): - """ - Test that default configuration is overrriden if custom configuration is provided - """ - retry_handler = RetryHandler( - max_retries=10, - retry_backoff_factor=0.2, - retry_backoff_max=200, - retry_time_limit=100, - retry_on_status_codes=[502, 503] - ) - - assert retry_handler.max_retries == 10 - assert retry_handler.timeout == 100 - assert retry_handler.backoff_max == 200 - assert retry_handler.backoff_factor == 0.2 - assert retry_handler._retry_on_status_codes == {429, 502, 503, 504} - - -def test_disable_retries(): - """ - Test that when disable_retries class method is called, total retries are set to zero - """ - retry_handler = RetryHandler() - retry_handler = retry_handler.disable_retries() - assert retry_handler.max_retries == 0 - retry_options = retry_handler.get_retry_options({}) - assert not retry_handler.check_retry_valid(retry_options, 0) - - -@responses.activate -def test_method_retryable_with_valid_method(): - """ - Test if method is retryable with a retryable request method. - """ - responses.add(responses.GET, BASE_URL, status=502) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler._is_method_retryable(settings, response.request) - - -@responses.activate -def test_should_retry_valid(): - """ - Test the should_retry method with a valid HTTP method and response code - """ - responses.add(responses.GET, BASE_URL, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler.should_retry(settings, response) - - -@responses.activate -def test_should_retry_invalid(): - """ - Test the should_retry method with an valid HTTP response code - """ - responses.add(responses.GET, BASE_URL, status=502) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert not retry_handler.should_retry(settings, response) - - -@responses.activate -def test_is_request_payload_buffered_valid(): - """ - Test for _is_request_payload_buffered helper method. - Should return true request payload is buffered/rewindable. - """ - responses.add(responses.GET, BASE_URL, status=429) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._is_request_payload_buffered(response) - - -@responses.activate -def test_is_request_payload_buffered_invalid(): - """ - Test for _is_request_payload_buffered helper method. - Should return false if request payload is forward streamed. - """ - responses.add(responses.POST, BASE_URL, status=429) - response = requests.post(BASE_URL, headers={'Content-Type': "application/octet-stream"}) - - retry_handler = RetryHandler() - - assert not retry_handler._is_request_payload_buffered(response) - - -def test_check_retry_valid(): - """ - Test that a retry is valid if the maximum number of retries has not been reached - """ - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler.check_retry_valid(settings, 0) - - -def test_check_retry_valid_no_retries(): - """ - Test that a retry is not valid if maximum number of retries has been reached - """ - retry_handler = RetryHandler(max_retries=2) - settings = retry_handler.get_retry_options({}) - - assert not retry_handler.check_retry_valid(settings, 2) - - -@responses.activate -def test_get_retry_after(): - """ - Test the _get_retry_after method with an integer value for retry header. - """ - responses.add(responses.GET, BASE_URL, headers={'Retry-After': "120"}, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) == 120 - - -@responses.activate -def test_get_retry_after_no_header(): - """ - Test the _get_retry_after method with no Retry-After header. - """ - responses.add(responses.GET, BASE_URL, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) is None - - -@responses.activate -def test_get_retry_after_http_date(): - """ - Test the _get_retry_after method with a http date as Retry-After value. - """ - timevalue = time() + 120 - http_date = formatdate(timeval=timevalue, localtime=False, usegmt=True) - responses.add(responses.GET, BASE_URL, headers={'retry-after': f'{http_date}'}, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) < 120 diff --git a/tests/unit/test_telemetry_handler.py b/tests/unit/test_telemetry_handler.py deleted file mode 100644 index 0d58eb93..00000000 --- a/tests/unit/test_telemetry_handler.py +++ /dev/null @@ -1,146 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import platform -import re -import uuid - -import pytest -import requests -import responses - -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.request_context import RequestContext -from msgraph.core.middleware.telemetry import TelemetryHandler - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -@responses.activate -def test_is_graph_url(): - """ - Test method that checks whether a request url is a graph endpoint - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - - telemetry_handler = TelemetryHandler() - assert telemetry_handler.is_graph_url(request.url) - - -@responses.activate -def test_is_not_graph_url(): - """ - Test method that checks whether a request url is a graph endpoint with a - non-graph url - """ - responses.add(responses.GET, 'https://httpbin.org/status/200') - response = requests.get('https://httpbin.org/status/200') - request = response.request - - telemetry_handler = TelemetryHandler() - assert not telemetry_handler.is_graph_url(request.url) - - -@responses.activate -def test_add_client_request_id_header(): - """ - Test that client_request_id is added to the request headers - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_client_request_id_header(request) - - assert 'client-request-id' in request.headers - assert _is_valid_uuid(request.headers.get('client-request-id')) - - -@responses.activate -def test_custom_client_request_id_header(): - """ - Test that a custom client request id is used, if provided - """ - custom_id = str(uuid.uuid4()) - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {'client-request-id': custom_id}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_client_request_id_header(request) - - assert 'client-request-id' in request.headers - assert _is_valid_uuid(request.headers.get('client-request-id')) - assert request.headers.get('client-request-id') == custom_id - - -@responses.activate -def test_append_sdk_version_header(): - """ - Test that sdkVersion is added to the request headers - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._append_sdk_version_header(request) - - assert 'sdkVersion' in request.headers - assert request.headers.get('sdkVersion').startswith('graph-python-core/' + SDK_VERSION) - - -@responses.activate -def test_add_host_os_header(): - """ - Test that HostOs is added to the request headers - """ - system = platform.system() - version = platform.version() - host_os = f'{system} {version}' - - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_host_os_header(request) - - assert 'HostOs' in request.headers - assert request.headers.get('HostOs') == host_os - - -@responses.activate -def test_add_runtime_environment_header(): - """ - Test that RuntimeEnvironment is added to the request headers - """ - python_version = platform.python_version() - runtime_environment = f'Python/{python_version}' - - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_runtime_environment_header(request) - - assert 'RuntimeEnvironment' in request.headers - assert request.headers.get('RuntimeEnvironment') == runtime_environment - - -def _is_valid_uuid(guid): - regex = "^[{]?[0-9a-fA-F]{8}" + "-([0-9a-fA-F]{4}-)" + "{3}[0-9a-fA-F]{12}[}]?$" - pattern = re.compile(regex) - if re.search(pattern, guid): - return True - return False