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 @@
-[](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions)
+[](https://badge.fury.io/py/msgraph-core)
+[](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions/workflows/build.yml)
[](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