├── .coveragerc ├── .devcontainer └── devcontainer.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 01-sdk-bug.yml │ ├── 02-sdk-feature-request.yml │ ├── 03-blank-issue.md │ └── config.yml ├── dependabot.yml ├── policies │ ├── msgraph-sdk-python-core-branch-protection.yml │ └── resourceManagement.yml ├── pull_request_template.md ├── release-please.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── conflicting-pr-label.yml │ ├── project-auto-add.yml │ └── publish.yml ├── .gitignore ├── .pylintrc ├── .release-please-manifest.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── docs ├── Makefile ├── conf.py ├── design │ ├── client_factory.puml │ └── request_context.puml ├── index.rst └── make.bat ├── pyproject.toml ├── release-please-config.json ├── requirements-dev.txt ├── samples ├── batch_requests │ ├── batch_get_response_body_as_stream.py │ ├── batch_request_with_collection.py │ ├── batch_request_with_content.py │ ├── batch_request_with_custom_error_class.py │ ├── batch_request_with_parsable_as_response_type.py │ └── batch_response_get_status_codes.py ├── client_factory_samples.py ├── graph_client_samples.py └── retry_handler_samples.py ├── src └── msgraph_core │ ├── __init__.py │ ├── _constants.py │ ├── _enums.py │ ├── authentication │ ├── __init__.py │ └── azure_identity_authentication_provider.py │ ├── base_graph_request_adapter.py │ ├── graph_client_factory.py │ ├── middleware │ ├── __init__.py │ ├── async_graph_transport.py │ ├── options │ │ ├── __init__.py │ │ └── graph_telemetry_handler_option.py │ ├── request_context.py │ └── telemetry.py │ ├── models │ ├── __init__.py │ ├── large_file_upload_session.py │ ├── page_result.py │ └── upload_result.py │ ├── py.typed.txt │ ├── requests │ ├── __init__.py │ ├── batch_request_builder.py │ ├── batch_request_content.py │ ├── batch_request_content_collection.py │ ├── batch_request_item.py │ ├── batch_response_content.py │ ├── batch_response_content_collection.py │ └── batch_response_item.py │ └── tasks │ ├── __init__.py │ ├── large_file_upload.py │ └── page_iterator.py └── tests ├── __init__.py ├── authentication └── test_azure_identity_authentication_provider.py ├── conftest.py ├── middleware ├── options │ └── test_graph_telemetry_handler_options.py ├── test_async_graph_transport.py └── test_graph_telemetry_handler.py ├── requests ├── __init__.py ├── test_batch_request_content.py ├── test_batch_request_content_collection.py ├── test_batch_request_item.py ├── test_batch_response_content.py └── test_batch_response_item.py ├── tasks ├── __init__.py ├── test_page_iterator.py └── test_page_result.py ├── test_base_graph_request_adapter.py └── test_graph_client_factory.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */site-packages/* 4 | */distutils/* -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | // "image": "mcr.microsoft.com/devcontainers/python:3.9-bookworm", 7 | // "image": "mcr.microsoft.com/devcontainers/python:3.10-bookworm", 8 | // "image": "mcr.microsoft.com/devcontainers/python:3.11-bookworm", 9 | // "image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm", 10 | // "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", 11 | // "image": "mcr.microsoft.com/devcontainers/python:3.13-bookworm", 12 | "image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye", 13 | 14 | "features": { 15 | "ghcr.io/hspaans/devcontainer-features/pytest:1": {}, 16 | "ghcr.io/devcontainers-extra/features/pylint:2": {}, 17 | "ghcr.io/devcontainers-extra/features/poetry:2": {} 18 | }, 19 | 20 | // Features to add to the dev container. More info: https://containers.dev/features. 21 | // "features": {}, 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | "postCreateCommand": "git config --global core.autocrlf true && pip3 install --user -r requirements-dev.txt", 28 | 29 | // Configure tool-specific properties. 30 | "customizations": { 31 | "vscode": { 32 | "extensions": ["ms-python.python"] 33 | } 34 | } 35 | 36 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 37 | // "remoteUser": "root" 38 | } 39 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoftgraph/msgraph-devx-python-write 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-sdk-bug.yml: -------------------------------------------------------------------------------- 1 | name: SDK Bug Report 2 | description: File SDK bug report 3 | labels: ["type:bug", "status:waiting-for-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Thank you for taking the time to fill out this bug report!** 9 | 💥Before submitting a new request, please search existing issues to see if an issue already exists. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the bug 14 | description: | 15 | Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or a screenshot. 16 | placeholder: I am trying to do [...] but [...] 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: expected-behavior 21 | attributes: 22 | label: Expected behavior 23 | description: | 24 | A clear and concise description of what you expected to happen. 25 | placeholder: Expected behavior 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: repro-steps 30 | attributes: 31 | label: How to reproduce 32 | description: | 33 | Please include minimal steps to reproduce the problem if possible. E.g.: the smallest possible code snippet; or steps to run project in link above. If possible include text as text rather than screenshots (so it shows up in searches). 34 | If there's a link to a public repo where the sample code exists, include it too. 35 | placeholder: Minimal Reproduction steps 36 | validations: 37 | required: true 38 | - type: input 39 | attributes: 40 | label: SDK Version 41 | placeholder: e.g. 5.32.1 42 | description: Version of the SDK with the bug described above. 43 | validations: 44 | required: false 45 | - type: input 46 | id: regression 47 | attributes: 48 | label: Latest version known to work for scenario above? 49 | description: | 50 | Did this work in a previous build or release of the SDK or API client? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK. 51 | placeholder: version-number 52 | validations: 53 | required: false 54 | - type: textarea 55 | id: known-workarounds 56 | attributes: 57 | label: Known Workarounds 58 | description: | 59 | Please provide a description of any known workarounds. 60 | placeholder: Known Workarounds 61 | validations: 62 | required: false 63 | - type: textarea 64 | id: logs 65 | attributes: 66 | label: Debug output 67 | description: Please copy and paste the debug output below. 68 | value: | 69 |
Click to expand log 70 | ``` 71 | 72 | 73 | 74 | ``` 75 |
76 | validations: 77 | required: false 78 | - type: textarea 79 | id: configuration 80 | attributes: 81 | label: Configuration 82 | description: | 83 | Please provide more information on your SDK configuration: 84 | * What OS and version, and what distro if applicable (Windows 10, Windows 11, MacOS Catalina, Ubuntu 22.04)? 85 | * What is the architecture (x64, x86, ARM, ARM64)? 86 | * Do you know whether it is specific to that configuration? 87 | placeholder: | 88 | - OS: 89 | - architecture: 90 | validations: 91 | required: false 92 | - type: textarea 93 | id: other-info 94 | attributes: 95 | label: Other information 96 | description: | 97 | If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. 98 | placeholder: Other information 99 | validations: 100 | required: false 101 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-sdk-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: SDK Feature request 2 | description: Request a new feature on the SDK 3 | labels: ["type:feature", "status:waiting-for-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Thank you for taking the time to fill out this feature request form!** 9 | 💥Please search to see if an issue already exists for the feature you are requesting. 10 | - type: textarea 11 | attributes: 12 | label: Is your feature request related to a problem? Please describe the problem. 13 | description: A clear and concise description of what the problem is. 14 | placeholder: I am trying to do [...] but [...] 15 | validations: 16 | required: false 17 | - type: textarea 18 | attributes: 19 | label: Describe the solution you'd like. 20 | description: | 21 | A clear and concise description of what you want to happen. Include any alternative solutions you've considered. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Additional context? 27 | description: | 28 | Add any other context or screenshots about the feature request here. 29 | validations: 30 | required: false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank issue 3 | about: Something that doesn't fit the other categories 4 | title: '' 5 | labels: ["status:waiting-for-triage"] 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Question on use of Python graph sdk 4 | url: https://github.com/microsoftgraph/msgraph-sdk-python-core/discussions 5 | about: Please add your question in the discussions section of the repo 6 | - name: Question on use of kiota 7 | url: https://github.com/microsoft/kiota/discussions 8 | about: Please add your question in the discussions section of the repo 9 | - name: Question or Feature Request for the MS Graph API? 10 | url: https://aka.ms/msgraphsupport 11 | about: Report an issue or limitation with the MS Graph service APIs 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | groups: 9 | open-telemetry: 10 | patterns: 11 | - "*opentelemetry*" 12 | kiota: 13 | patterns: 14 | - "*kiota*" 15 | pylint: 16 | patterns: 17 | - "*pylint*" 18 | - "*astroid*" 19 | - package-ecosystem: github-actions 20 | directory: "/" 21 | schedule: 22 | interval: daily 23 | open-pull-requests-limit: 10 24 | -------------------------------------------------------------------------------- /.github/policies/msgraph-sdk-python-core-branch-protection.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | name: msgraph-sdk-python-core-branch-protection 5 | description: Branch protection policy for the msgraph-sdk-python-core repository 6 | resource: repository 7 | configuration: 8 | branchProtectionRules: 9 | 10 | - branchNamePattern: main 11 | # This branch pattern applies to the following branches as of 01/12/2024 13:14:28: 12 | # main 13 | 14 | # Specifies whether this branch can be deleted. boolean 15 | allowsDeletions: false 16 | # Specifies whether forced pushes are allowed on this branch. boolean 17 | allowsForcePushes: true 18 | # Specifies whether new commits pushed to the matching branches dismiss pull request review approvals. boolean 19 | dismissStaleReviews: true 20 | # Specifies whether admins can overwrite branch protection. boolean 21 | isAdminEnforced: false 22 | # Indicates whether "Require a pull request before merging" is enabled. boolean 23 | requiresPullRequestBeforeMerging: true 24 | # Specifies the number of pull request reviews before merging. int (0-6). Should be null/empty if PRs are not required 25 | requiredApprovingReviewsCount: 1 26 | # Require review from Code Owners. Requires requiredApprovingReviewsCount. boolean 27 | requireCodeOwnersReview: true 28 | # Are commits required to be signed. boolean. TODO: all contributors must have commit signing on local machines. 29 | requiresCommitSignatures: false 30 | # Are conversations required to be resolved before merging? boolean 31 | requiresConversationResolution: true 32 | # Are merge commits prohibited from being pushed to this branch. boolean 33 | requiresLinearHistory: false 34 | # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any 35 | # existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status 36 | requiredStatusChecks: 37 | - CodeQL 38 | - check-build-matrix 39 | - license/cla 40 | # Require branches to be up to date before merging. Requires requiredStatusChecks. boolean 41 | requiresStrictStatusChecks: true 42 | # Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush. 43 | restrictsPushes: false 44 | # Restrict who can dismiss pull request reviews. boolean 45 | restrictsReviewDismissals: false 46 | -------------------------------------------------------------------------------- /.github/policies/resourceManagement.yml: -------------------------------------------------------------------------------- 1 | id: 2 | name: GitOps.PullRequestIssueManagement 3 | description: GitOps.PullRequestIssueManagement primitive 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: 12 | frequencies: 13 | - hourly: 14 | hour: 6 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: 'status:waiting-for-author-feedback' 20 | - hasLabel: 21 | label: 'Status: No Recent Activity' 22 | - noActivitySince: 23 | days: 3 24 | actions: 25 | - closeIssue 26 | - description: 27 | frequencies: 28 | - hourly: 29 | hour: 6 30 | filters: 31 | - isIssue 32 | - isOpen 33 | - hasLabel: 34 | label: 'status:waiting-for-author-feedback' 35 | - noActivitySince: 36 | days: 4 37 | - isNotLabeledWith: 38 | label: 'Status: No Recent Activity' 39 | actions: 40 | - addLabel: 41 | label: 'Status: No Recent Activity' 42 | - addReply: 43 | 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**. 44 | - description: 45 | frequencies: 46 | - hourly: 47 | hour: 6 48 | filters: 49 | - isIssue 50 | - isOpen 51 | - hasLabel: 52 | label: 'duplicate' 53 | - noActivitySince: 54 | days: 1 55 | actions: 56 | - addReply: 57 | reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. 58 | - closeIssue 59 | eventResponderTasks: 60 | - if: 61 | - payloadType: Issues 62 | - isAction: 63 | action: Closed 64 | - hasLabel: 65 | label: 'status:waiting-for-author-feedback' 66 | then: 67 | - removeLabel: 68 | label: 'status:waiting-for-author-feedback' 69 | description: 70 | - if: 71 | - payloadType: Issue_Comment 72 | - isAction: 73 | action: Created 74 | - isActivitySender: 75 | issueAuthor: True 76 | - hasLabel: 77 | label: 'status:waiting-for-author-feedback' 78 | - isOpen 79 | then: 80 | - addLabel: 81 | label: 'Needs: Attention :wave:' 82 | - removeLabel: 83 | label: 'status:waiting-for-author-feedback' 84 | description: 85 | - if: 86 | - payloadType: Issues 87 | - not: 88 | isAction: 89 | action: Closed 90 | - hasLabel: 91 | label: 'Status: No Recent Activity' 92 | then: 93 | - removeLabel: 94 | label: 'Status: No Recent Activity' 95 | description: 96 | - if: 97 | - payloadType: Issue_Comment 98 | - hasLabel: 99 | label: 'Status: No Recent Activity' 100 | then: 101 | - removeLabel: 102 | label: 'Status: No Recent Activity' 103 | description: 104 | - if: 105 | - payloadType: Pull_Request 106 | then: 107 | - inPrLabel: 108 | label: WIP 109 | description: 110 | onFailure: 111 | onSuccess: 112 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Brief description of what this PR does, and why it is needed. 4 | 5 | ### Demo 6 | 7 | Optional. Screenshots, `curl` examples, etc. 8 | 9 | ### Notes 10 | 11 | Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. 12 | 13 | ## Testing Instructions 14 | 15 | * How to test this PR 16 | * Prefer bulleted description 17 | * Start after checking out this branch 18 | * Include any setup required, such as bundling scripts, restarting services, etc. 19 | * Include test case, and expected output -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | manifest: true 2 | primaryBranch: main 3 | handleGHRelease: true 4 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge dependabot updates 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | dependabot-merge: 13 | runs-on: ubuntu-latest 14 | 15 | if: ${{ github.actor == 'dependabot[bot]' }} 16 | 17 | steps: 18 | - name: Dependabot metadata 19 | id: metadata 20 | uses: dependabot/fetch-metadata@v2.4.0 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | 24 | - name: Enable auto-merge for Dependabot PRs 25 | # Only if version bump is not a major version change 26 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 27 | run: gh pr merge --auto --merge "$PR_URL" 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build and test 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | workflow_call: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 40 17 | strategy: 18 | max-parallel: 5 19 | matrix: 20 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install -r requirements-dev.txt 32 | - name: Check code format 33 | run: | 34 | yapf -dr src 35 | - name: Check import order 36 | run: | 37 | isort src 38 | - name: Static type checking with Mypy 39 | run: | 40 | mypy src 41 | - name: Lint with Pylint 42 | run: | 43 | pylint src --disable=W --rcfile=.pylintrc 44 | - name: Test with pytest 45 | run: | 46 | pytest 47 | env: 48 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} 49 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 50 | AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} 51 | 52 | # The check-build-matrix returns success if all matrix jobs in build are successful; otherwise, it returns a failure. 53 | # Use this as a PR status check for GitHub Policy Service instead of individual matrix entry checks. 54 | check-build-matrix: 55 | runs-on: ubuntu-latest 56 | needs: build 57 | if: always() 58 | steps: 59 | - name: All build matrix options are successful 60 | if: ${{ !(contains(needs.*.result, 'failure')) }} 61 | run: exit 0 62 | - name: One or more build matrix options failed 63 | if: ${{ contains(needs.*.result, 'failure') }} 64 | run: exit 1 65 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: "32 11 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["python"] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v3 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | 50 | # 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 51 | # queries: security-extended,security-and-quality 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # If the Autobuild fails above, remove it and uncomment the following three lines. 62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 63 | 64 | # - run: | 65 | # echo "Run, Build Application using script" 66 | # ./location_of_script_within_repo/buildscript.sh 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v3 70 | -------------------------------------------------------------------------------- /.github/workflows/conflicting-pr-label.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: PullRequestConflicting 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | types: [synchronize] 12 | branches: [main] 13 | 14 | permissions: 15 | pull-requests: write 16 | contents: read 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | # This workflow contains a single job called "build" 21 | build: 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-latest 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | - name: check if prs are dirty 28 | uses: eps1lon/actions-label-merge-conflict@releases/2.x 29 | if: env.LABELING_TOKEN != '' && env.LABELING_TOKEN != null 30 | id: check 31 | with: 32 | dirtyLabel: "conflicting" 33 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 34 | continueOnMissingPermissions: true 35 | commentOnDirty: "This pull request has conflicting changes, the author must resolve the conflicts before this pull request can be merged." 36 | commentOnClean: "Conflicts have been resolved. A maintainer will take a look shortly." 37 | env: 38 | LABELING_TOKEN: ${{secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/project-auto-add.yml: -------------------------------------------------------------------------------- 1 | # This workflow is used to add new issues to GitHub GraphSDKs Project 2 | 3 | name: Add Issue or PR to project 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request: 9 | types: 10 | - opened 11 | branches: 12 | - "main" 13 | 14 | jobs: 15 | track_issue: 16 | if: github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.fork == false 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Generate token 20 | id: generate_token 21 | uses: actions/create-github-app-token@v2 22 | with: 23 | app-id: ${{ secrets.GRAPHBOT_APP_ID }} 24 | private-key: ${{ secrets.GRAPHBOT_APP_PEM }} 25 | 26 | - name: Get project data 27 | env: 28 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 29 | ORGANIZATION: microsoftgraph 30 | PROJECT_NUMBER: 55 31 | run: | 32 | gh api graphql -f query=' 33 | query($org: String!, $number: Int!) { 34 | organization(login: $org){ 35 | projectV2(number: $number) { 36 | id 37 | fields(first:20) { 38 | nodes { 39 | ... on ProjectV2SingleSelectField { 40 | id 41 | name 42 | options { 43 | id 44 | name 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json 52 | 53 | echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV 54 | echo 'LANGUAGE_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Language") | .id' project_data.json) >> $GITHUB_ENV 55 | echo 'LANGUAGE_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Language") | .options[] | select(.name=="Python") |.id' project_data.json) >> $GITHUB_ENV 56 | 57 | - name: Add Issue or PR to project 58 | env: 59 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 60 | ISSUE_ID: ${{ github.event_name == 'issues' && github.event.issue.node_id || github.event.pull_request.node_id }} 61 | run: | 62 | item_id="$( gh api graphql -f query=' 63 | mutation($project:ID!, $issue:ID!) { 64 | addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { 65 | item { 66 | id 67 | } 68 | } 69 | }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" 70 | 71 | echo 'ITEM_ID='$item_id >> $GITHUB_ENV 72 | 73 | - name: Set Language 74 | env: 75 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 76 | run: | 77 | gh api graphql -f query=' 78 | mutation ( 79 | $project: ID! 80 | $item: ID! 81 | $language_field: ID! 82 | $language_value: String! 83 | ) { 84 | set_status: updateProjectV2ItemFieldValue(input: { 85 | projectId: $project 86 | itemId: $item 87 | fieldId: $language_field 88 | value: {singleSelectOptionId: $language_value} 89 | }) { 90 | projectV2Item { 91 | id 92 | } 93 | } 94 | }' -f project=$PROJECT_ID -f item=$ITEM_ID -f language_field=$LANGUAGE_FIELD_ID -f language_value=${{ env.LANGUAGE_OPTION_ID }} --silent 95 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to PyPI and create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/build.yml 11 | 12 | publish: 13 | name: Publish distribution to PyPI 14 | runs-on: ubuntu-latest 15 | if: startsWith(github.ref, 'refs/tags/v') 16 | environment: pypi_prod 17 | needs: [build] 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | - name: Set up Python 3.13 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.13 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build 29 | - name: Build package 30 | run: python -m build 31 | - name: Publish package 32 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc 33 | with: 34 | user: __token__ 35 | password: ${{ secrets.PYPI_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | *env/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Pycharm 133 | .idea/ 134 | 135 | app*.py 136 | app* 137 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.3.4" 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.testing.pytestArgs": [ 4 | "tests" 5 | ], 6 | "python.testing.unittestEnabled": false, 7 | "python.testing.pytestEnabled": true 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.3.4](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.3.3...v1.3.4) (2025-06-02) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * dependecy conflict ([914da4f](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/914da4fc7c5a65a80fbf2411cc5ab4cf333a5e14)) 11 | 12 | ## [1.3.3](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.3.2...v1.3.3) (2025-03-24) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * sequencing of batches ([9e14ea3](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/9e14ea3bb3126df47a62ed026ecbd5af471a15e3)) 18 | 19 | ## [1.3.2](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.3.1...v1.3.2) (2025-02-25) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * Batch building bugs ([#837](https://github.com/microsoftgraph/msgraph-sdk-python-core/issues/837)) ([a0dd3c1](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/a0dd3c18d39f9cadbba25109345ee7be8a810a99)) 25 | 26 | ## [1.3.1](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.3.0...v1.3.1) (2025-02-06) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * bump kiota dependencies ([#827](https://github.com/microsoftgraph/msgraph-sdk-python-core/issues/827)) ([466b5c1](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/466b5c119851debfcf5a1ab6d36f145abc36caaf)) 32 | 33 | ## [1.3.0](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.2.1...v1.3.0) (2025-02-03) 34 | 35 | 36 | ### Features 37 | 38 | * adds support for python 3.13 ([b972a20](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/b972a2073cd7d272455161209da4f031b50e7a2d)) 39 | 40 | ## [1.2.1](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.2.0...v1.2.1) (2025-01-30) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * issue with national cloud/version enums and base url being set [#818](https://github.com/microsoftgraph/msgraph-sdk-python-core/issues/818) ([4ee7887](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/4ee78873cfe176c38e84c4f1d7f469c73eb6dff6)) 46 | 47 | ## [1.2.0](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.8...v1.2.0) (2025-01-14) 48 | 49 | 50 | ### Features 51 | 52 | * drop support for python 3.8 and drops deprecated type aliases ([d5d925e](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/d5d925e35d1a3a20be3e106ee55ec253d1599ebd)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * drop support for python 3.8 and drops deprecated type aliases ([8c11aaf](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/8c11aaf50f9b62bca2b863b0881dc3ccde7e9f37)) 58 | 59 | ## [1.1.8](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.7...v1.1.8) (2024-12-18) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * Fixes type hints and failing mypy checks ([85e8935](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/85e8935971adef13d4f5d1e55970c570ad267dda)) 65 | 66 | ## [1.1.7](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.6...v1.1.7) (2024-11-21) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * depends_on and batch request with content collection ([9d4153a](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/9d4153a1b0ce29ed0213f81bb4bd3191125304e5)) 72 | 73 | ## [1.1.6](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.5...v1.1.6) (2024-10-18) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * removes the tests directory from the package ([3d919a7](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/3d919a7f88c82bcebcbe093d9606906b56e0b416)) 79 | * removes the tests directory from the package ([ccbed8d](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/ccbed8df3a9d9165b81f2f8af80282eeb2814907)) 80 | 81 | ## [1.1.5](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.4...v1.1.5) (2024-10-02) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * release please initial configuration ([e781cd8](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/e781cd81156622b59a5b3c48fdf70995379d08a0)) 87 | 88 | ## [1.1.4](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.3...v1.1.4) (2024-09-24) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * Use abstractions request adapter in tasks ([6d390a2](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/6d390a2a5dea74d137f907cabf8b520100c5b1a8)) 94 | 95 | ## [1.1.3](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.2...v1.1.3) (2024-09-03) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * remove print statements from upload code. ([353d72d](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/353d72da513e0c5b809d31a8d921de0a0bde10be)) 101 | 102 | ## [1.1.2](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.1...v1.1.2) (2024-07-12) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * adds missing whitespace for suppressions ([7ad013e](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/7ad013e52190ab607dfe82c86ae68c87e7abe4cc)) 108 | * fixes exception configuration in pylint ([857ad99](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/857ad9950a0200dbe69d4b96052725624fbe8833)) 109 | * linting fix import ordering ([b56cc8d](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/b56cc8d11221166fbd4c8002dfdf9eb027968b5e)) 110 | * linting missing line ([f39f0b9](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/f39f0b9b68f94b91c6b179f5f65db960922ecc77)) 111 | * moves attributes suppression to class definition ([b6c1d29](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/b6c1d29777829aedf50cadf994e9b8ea68d8ed4a)) 112 | * suppressed linting error message that fails dependencies ([d7c0e1b](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/d7c0e1b901ffb0970eb6b94dd623dda41995a772)) 113 | 114 | ## [1.1.1](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.1.0...v1.1.1) (2024-07-10) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * avoid using default mutable parameters ([9fa773a](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/9fa773a7ca92f916a6eb593f033322d5a1918a10)) 120 | * fixes constants path for release please config ([2ff4440](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/2ff4440a18347feb173a40010ab4d9910717c6b6)) 121 | 122 | ## [1.1.0](https://github.com/microsoftgraph/msgraph-sdk-python-core/compare/v1.0.1...v1.1.0) (2024-06-19) 123 | 124 | 125 | ### Features 126 | 127 | * adds support for python 3.12 ([991a5e0](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/991a5e0bc2ea4db108a127a1d079967b97ae1280)) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * removes unecessary printout in large file upload task. 133 | * replaces older contributing rst by md version ([70f6fb2](https://github.com/microsoftgraph/msgraph-sdk-python-core/commit/70f6fb25e612b7d01abba27c6c43ca43f166dcbf)) 134 | 135 | ## [1.0.1] - 2024-04-22 136 | 137 | ### Added 138 | 139 | ### Changed 140 | 141 | -Enabled Large File Upload and Page iterator support 142 | 143 | ## [1.0.0] - 2023-10-31 144 | 145 | ### Added 146 | 147 | ### Changed 148 | 149 | - GA release. 150 | 151 | ## [1.0.0a6] - 2023-10-12 152 | 153 | ### Added 154 | 155 | ### Changed 156 | 157 | - Replaced default transport with graph transport when using custom client with proxy. 158 | 159 | ## [1.0.0a5] - 2023-06-20 160 | 161 | ### Added 162 | 163 | - Added `AzureIdentityAuthenticationProvider` that sets the default scopes and allowed hosts. 164 | 165 | ### Changed 166 | 167 | - Changed the documentation in the README to show how to use `AzureIdentityAuthenticationProvider` from the core SDK. 168 | 169 | ## [1.0.0a4] - 2023-02-02 170 | 171 | ### Added 172 | 173 | ### Changed 174 | 175 | - Enabled configuring of middleware during client creation by passing custom options in call to create with default middleware. 176 | - Fixed a bug where the transport would check for request options even after they have been removed after final middleware execution. 177 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Microsoft Graph Core SDK for Python 2 | 3 | The Microsoft Graph SDK for python is available for all manner of contribution. There are a couple of different recommended paths to get contributions into the released version of this SDK. 4 | 5 | __NOTE__ A signed a contribution license agreement is required for all contributions, and is checked automatically on new pull requests. Please read and sign [the agreement](https://cla.microsoft.com/) before starting any work for this repository. 6 | 7 | ## File issues 8 | 9 | The best way to get started with a contribution is to start a dialog with the owners of this repository. Sometimes features will be under development or out of scope for this SDK and it's best to check before starting work on contribution. 10 | 11 | ## Submit pull requests for trivial changes 12 | 13 | If you are making a change that does not affect the interface components and does not affect other downstream callers, feel free to make a pull request against the __main__ branch. The main branch will be updated frequently. 14 | 15 | Revisions of this nature will result in a 0.0.X change of the version number. 16 | 17 | ## Submit pull requests for features 18 | 19 | If major functionality is being added, or there will need to be gestation time for a change, it should be submitted against the __feature__ branch. 20 | 21 | Revisions of this nature will result in a 0.X.X change of the version number. 22 | 23 | ## Commit message format 24 | 25 | To support our automated release process, pull requests are required to follow the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) 26 | format. 27 | 28 | Each commit message consists of a **header**, an optional **body** and an optional **footer**. The header is the first line of the commit and 29 | MUST have a **type** (see below for a list of types) and a **description**. An optional **scope** can be added to the header to give extra context. 30 | 31 | ``` 32 | [optional scope]: 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | The recommended commit types used are: 40 | 41 | - **feat** for feature updates (increments the _minor_ version) 42 | - **fix** for bug fixes (increments the _patch_ version) 43 | - **perf** for performance related changes e.g. optimizing an algorithm 44 | - **refactor** for code refactoring changes 45 | - **test** for test suite updates e.g. adding a test or fixing a test 46 | - **style** for changes that don't affect the meaning of code. e.g. formatting changes 47 | - **docs** for documentation updates e.g. ReadMe update or code documentation updates 48 | - **build** for build system changes (gradle updates, external dependency updates) 49 | - **ci** for CI configuration file changes e.g. updating a pipeline 50 | - **chore** for miscallaneous non-sdk changesin the repo e.g. removing an unused file 51 | 52 | Adding a footer with the prefix **BREAKING CHANGE:** will cause an increment of the _major_ version. 53 | 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Microsoft Graph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude tests/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/msgraph-core.svg)](https://badge.fury.io/py/msgraph-core) 2 | [![CI Actions Status](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions/workflows/build.yml/badge.svg)](https://github.com/microsoftgraph/msgraph-sdk-python-core/actions/workflows/build.yml) 3 | [![Downloads](https://pepy.tech/badge/msgraph-core)](https://pepy.tech/project/msgraph-core) 4 | 5 | ## Microsoft Graph Core Python Client Library 6 | 7 | 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). 8 | 9 | > NOTE: 10 | > 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). 11 | > 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. 12 | 13 | ## Prerequisites 14 | 15 | Python 3.9+ 16 | 17 | This library doesn't support [older](https://devguide.python.org/versions/) versions of Python. 18 | 19 | ## Getting started 20 | 21 | ### 1. Register your application 22 | 23 | To call Microsoft Graph, your app must acquire an access token from the Microsoft identity platform. Learn more about this - 24 | 25 | - [Authentication and authorization basics for Microsoft Graph](https://docs.microsoft.com/en-us/graph/auth/auth-concepts) 26 | - [Register your app with the Microsoft identity platform](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) 27 | 28 | ### 2. Install the required packages 29 | 30 | msgraph-core is available on PyPI. 31 | 32 | ```cmd 33 | pip3 install msgraph-core 34 | pip3 install azure-identity 35 | ``` 36 | 37 | ### 3. Configure an Authentication Provider Object 38 | 39 | 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. 40 | 41 | > **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`. 42 | 43 | ```py 44 | # Using EnvironmentCredential for demonstration purposes. 45 | # There are many other options for getting an access token. See the following for more information. 46 | # https://pypi.org/project/azure-identity/#async-credentials 47 | from azure.identity.aio import EnvironmentCredential 48 | from msgraph_core.authentication import AzureIdentityAuthenticationProvider 49 | 50 | credential=EnvironmentCredential() 51 | auth_provider = AzureIdentityAuthenticationProvider(credential) 52 | ``` 53 | 54 | > **Note**: `AzureIdentityAuthenticationProvider` sets the default scopes and allowed hosts. 55 | 56 | ### 5. Pass the authentication provider object to the BaseGraphRequestAdapter constructor 57 | 58 | ```python 59 | from msgraph_core import BaseGraphRequestAdapter 60 | adapter = BaseGraphRequestAdapter(auth_provider) 61 | ``` 62 | 63 | ### 6. Make a requests to the graph 64 | 65 | After you have a `BaseGraphRequestAdapter` that is authenticated, you can begin making calls against the service. 66 | 67 | ```python 68 | import asyncio 69 | from kiota_abstractions.request_information import RequestInformation 70 | 71 | request_info = RequestInformation() 72 | request_info.url = 'https://graph.microsoft.com/v1.0/me' 73 | 74 | # User is your own type that implements Parsable or comes from the service library 75 | user = asyncio.run(adapter.send_async(request_info, User, {})) 76 | print(user.display_name) 77 | ``` 78 | 79 | ## Telemetry Metadata 80 | 81 | This library captures metadata by default that provides insights into its usage and helps to improve the developer experience. This metadata includes the `SdkVersion`, `RuntimeEnvironment` and `HostOs` on which the client is running. 82 | 83 | ## Issues 84 | 85 | View or log issues on the [Issues](https://github.com/microsoftgraph/msgraph-sdk-python-core/issues) tab in the repo. 86 | 87 | ## Contributing 88 | 89 | Please see the [contributing guidelines](CONTRIBUTING.md). 90 | 91 | ## Copyright and license 92 | 93 | Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT [license](LICENSE). 94 | 95 | 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. 96 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **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. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'msgraph-sdk-python' 20 | copyright = '2020, Microsoft' 21 | author = 'Microsoft' 22 | 23 | # The full version, including alpha/beta/rc tags 24 | release = '0.0.1' 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # List of patterns, relative to source directory, that match files and 37 | # directories to ignore when looking for source files. 38 | # This pattern also affects html_static_path and html_extra_path. 39 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | 43 | # The theme to use for HTML and HTML Help pages. See the documentation for 44 | # a list of builtin themes. 45 | # 46 | html_theme = 'alabaster' 47 | 48 | # Add any paths that contain custom static files (such as style sheets) here, 49 | # relative to this directory. They are copied after the builtin static files, 50 | # so a file named "default.css" will overwrite the builtin "default.css". 51 | html_static_path = ['_static'] 52 | -------------------------------------------------------------------------------- /docs/design/client_factory.puml: -------------------------------------------------------------------------------- 1 | @startuml ClientFactory 2 | enum NationalClouds { 3 | +GERMANY 4 | +PUBLIC 5 | +US_GOV 6 | +CHINA 7 | } 8 | 9 | class HttpClientFactory { 10 | -TIMEOUT: string 11 | -SDK_VERSION: string 12 | -BASE_URL: string 13 | -pipeline: MiddlewarePipeline 14 | 15 | +__init__(session: Session, cloud: NationalClouds) 16 | +with_default_middleware(auth_provider: TokenCredential): Session 17 | +with_user_middleware(middleware: [Middleware]): Session 18 | } 19 | 20 | 21 | class Session {} 22 | 23 | class GraphClient { 24 | -session: Session 25 | 26 | +__init__(session: Session, credential: TokenCredential, 27 | version: ApiVersion, cloud: NationalClouds) 28 | +get() 29 | +post() 30 | +put() 31 | +patch() 32 | +delete() 33 | } 34 | 35 | package "middleware" { 36 | class MiddlewarePipeline {} 37 | } 38 | 39 | HttpClientFactory --> NationalClouds 40 | HttpClientFactory -right-> middleware 41 | HttpClientFactory --> Session 42 | 43 | GraphClient -right-> HttpClientFactory 44 | 45 | note right of Session: HTTPClient imported from requests 46 | @enduml -------------------------------------------------------------------------------- /docs/design/request_context.puml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-python-core/849b14ae70ae509f12fe1f983db859f099335580/docs/design/request_context.puml -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. msgraph-sdk-python documentation master file, created by 2 | sphinx-quickstart on Mon Mar 2 10:35:08 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to msgraph-sdk-python's documentation! 7 | ============================================== 8 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=65.5.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "msgraph-core" 7 | # The SDK version 8 | # x-release-please-start-version 9 | version = "1.3.4" 10 | # x-release-please-end 11 | authors = [{name = "Microsoft", email = "graphtooling+python@microsoft.com"}] 12 | description = "Core component of the Microsoft Graph Python SDK" 13 | dependencies = [ 14 | "microsoft-kiota-abstractions >=1.8.0,<2.0.0", 15 | "microsoft-kiota-authentication-azure >=1.8.0,<2.0.0", 16 | "microsoft-kiota-http >=1.8.0,<2.0.0", 17 | "httpx[http2] >=0.23.0", 18 | ] 19 | requires-python = ">=3.9" 20 | license = {file = "LICENSE"} 21 | readme = "README.md" 22 | keywords = ["msgraph", "openAPI", "Microsoft", "Graph"] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "License :: OSI Approved :: MIT License", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = ["yapf", "bumpver", "isort", "pylint", "pytest", "mypy"] 35 | 36 | [project.urls] 37 | homepage = "https://github.com/microsoftgraph/msgraph-sdk-python-core#readme" 38 | repository = "https://github.com/microsoftgraph/msgraph-sdk-python-core" 39 | documentation = "https://github.com/microsoftgraph/msgraph-sdk-python-core/docs" 40 | 41 | [tool.mypy] 42 | warn_unused_configs = true 43 | files = "src" 44 | 45 | [tool.yapf] 46 | based_on_style = "pep8" 47 | dedent_closing_brackets = true 48 | each_dict_entry_on_separate_line = true 49 | column_limit = 100 50 | 51 | [tool.isort] 52 | profile = "hug" 53 | 54 | [tool.pytest.ini_options] 55 | pythonpath = [ 56 | "src" 57 | ] 58 | 59 | [tool.bumpver] 60 | current_version = "1.0.0" 61 | version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" 62 | commit_message = "bump version {old_version} -> {new_version}" 63 | commit = true 64 | tag = false 65 | push = false 66 | 67 | [tool.bumpver.file_patterns] 68 | "pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"'] 69 | "src/msgraph_core/_constants.py" = ["{version}"] 70 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "991a5e0bc2ea4db108a127a1d079967b97ae1280", 3 | "exclude-paths": [ 4 | ".git", 5 | ".idea", 6 | ".github", 7 | ".vscode" 8 | ], 9 | "release-type": "simple", 10 | "include-component-in-tag": false, 11 | "include-v-in-tag": true, 12 | "packages": { 13 | ".": { 14 | "package-name": "msgraph-core", 15 | "changelog-path": "CHANGELOG.md", 16 | "extra-files": [ 17 | "pyproject.toml", 18 | "src/msgraph_core/_constants.py" 19 | ] 20 | } 21 | }, 22 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 23 | } -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | 3 | async-generator==1.10 ; python_version >= '3.5' 4 | 5 | asyncmock==0.4.2 6 | 7 | attrs==25.3.0 ; python_version >= '3.7' 8 | 9 | azure-core==1.34.0 ; python_version >= '3.7' 10 | 11 | azure-identity==1.23.0 12 | 13 | build==1.2.2.post1 14 | 15 | bumpver==2024.1130 16 | 17 | certifi==2025.4.26 ; python_version >= '3.6' 18 | 19 | cffi==1.17.1 ; os_name == 'nt' and implementation_name != 'pypy' 20 | 21 | charset-normalizer==3.4.2 ; python_full_version >= '3.7.0' 22 | 23 | click==8.1.8 ; python_version >= '3.6' 24 | 25 | colorama==0.4.6 ; os_name == 'nt' 26 | 27 | coverage[toml]==7.8.2 ; python_version >= '3.7' 28 | 29 | cryptography==45.0.3 ; python_version >= '3.7' 30 | 31 | dill==0.4.0 ; python_version < '3.11' 32 | 33 | exceptiongroup==1.3.0 ; python_version < '3.11' 34 | 35 | idna==3.10 ; python_version >= '3.5' 36 | 37 | importlib-metadata==7.1.0 ; python_version >= '3.7' 38 | 39 | iniconfig==2.1.0 ; python_version >= '3.7' 40 | 41 | isort==6.0.1 42 | 43 | lazy-object-proxy==1.11.0 ; python_version >= '3.7' 44 | 45 | lexid==2021.1006 ; python_version >= '2.7' 46 | 47 | looseversion==1.3.0 ; python_version >= '3.5' 48 | 49 | mccabe==0.7.0 ; python_version >= '3.6' 50 | 51 | mock==5.2.0 ; python_version >= '3.6' 52 | 53 | msal==1.32.3 54 | 55 | msal-extensions==1.3.1 56 | 57 | mypy==1.16.0 58 | 59 | mypy-extensions==1.1.0 ; python_version >= '3.5' 60 | 61 | outcome==1.3.0.post0 ; python_version >= '3.7' 62 | 63 | packaging==25.0 ; python_version >= '3.7' 64 | 65 | pathlib2==2.3.7.post1 66 | 67 | platformdirs==4.3.8 ; python_version >= '3.7' 68 | 69 | pluggy==1.6.0 ; python_version >= '3.7' 70 | 71 | portalocker==2.10.1 ; python_version >= '3.5' and platform_system == 'Windows' 72 | 73 | pycparser==2.22 74 | 75 | pyjwt[crypto]==2.9.0 ; python_version >= '3.7' 76 | 77 | pylint==3.3.7 78 | 79 | pyproject-hooks==1.2.0 ; python_version >= '3.7' 80 | 81 | pytest==8.4.0 82 | 83 | pytest-cov==5.0.0 84 | 85 | pytest-mock==3.14.1 86 | 87 | python-dotenv==1.1.0 88 | 89 | pytest-trio==0.8.0 90 | 91 | pytest-asyncio==0.26.0 92 | 93 | pywin32==310 ; platform_system == 'Windows' 94 | 95 | requests==2.32.3 ; python_version >= '3.7' 96 | 97 | setuptools==80.9.0 98 | 99 | six==1.17.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 100 | 101 | sniffio==1.3.1 ; python_version >= '3.7' 102 | 103 | sortedcontainers==2.4.0 104 | 105 | toml==0.10.2 106 | 107 | tomli==2.2.1 ; python_version < '3.11' 108 | 109 | tomlkit==0.13.3 ; python_version >= '3.7' 110 | 111 | trio==0.30.0 112 | 113 | types-python-dateutil==2.9.0.20250516 114 | 115 | types-requests==2.32.0.20250602; python_version >= '3.7' 116 | urllib3==2.4.0 ; python_version >= '3.7' 117 | typing-extensions==4.13.2 ; python_version >= '3.7' 118 | 119 | 120 | wrapt==1.17.2 ; python_version < '3.11' 121 | 122 | yapf==0.43.0 123 | 124 | zipp==3.22.0 ; python_version >= '3.7' 125 | 126 | aiohttp==3.12.9 ; python_version >= '3.6' 127 | 128 | aiosignal==1.3.1 ; python_version >= '3.7' 129 | 130 | anyio==4.9.0 ; python_version >= '3.7' 131 | 132 | async-timeout==5.0.1 ; python_version >= '3.6' 133 | 134 | frozenlist==1.6.2 ; python_version >= '3.7' 135 | 136 | h11==0.16.0 ; python_version >= '3.7' 137 | 138 | h2==4.2.0 139 | 140 | hpack==4.1.0 ; python_full_version >= '3.6.1' 141 | 142 | httpcore==1.0.9 ; python_version >= '3.7' 143 | 144 | httpx[http2]==0.28.1 145 | 146 | hyperframe==6.1.0 ; python_full_version >= '3.6.1' 147 | 148 | microsoft-kiota-abstractions==1.9.3 149 | 150 | microsoft-kiota-authentication-azure==1.9.3 151 | 152 | microsoft-kiota-http==1.9.3 153 | 154 | multidict==6.4.4 ; python_version >= '3.7' 155 | 156 | uritemplate==4.2.0 ; python_version >= '3.6' 157 | 158 | yarl==1.20.0 ; python_version >= '3.7' 159 | 160 | deprecated==1.2.18 161 | 162 | types-Deprecated==1.2.15.20250304 163 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_get_response_body_as_stream.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """ Demonstrate getting response body as stream in Batch Responses""" 7 | batch_request_content = { 8 | batch_request_item1.id: batch_request_item1, 9 | batch_request_item2.id: batch_request_item2 10 | } 11 | 12 | batch_content = BatchRequestContent(batch_request_content) 13 | 14 | 15 | async def main(): 16 | batch_response_content = await client.batch.post(batch_request_content=batch_content) 17 | 18 | try: 19 | stream_response = batch_response_content.get_response_stream_by_id(batch_request_item1.id) 20 | print(f"Stream Response: {stream_response}") 21 | print(f"Stream Response Content: {stream_response.read()}") 22 | except AttributeError as e: 23 | print(f"Error getting response by ID: {e}") 24 | 25 | 26 | asyncio.run(main()) 27 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_request_with_collection.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the Batch request with Collection""" 7 | import asyncio 8 | 9 | from urllib.request import Request 10 | from kiota_abstractions.request_information import RequestInformation 11 | 12 | from msgraph import GraphServiceClient 13 | 14 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 15 | from msgraph_core.requests.batch_request_item import BatchRequestItem 16 | 17 | from msgraph_core.requests.batch_request_content import BatchRequestContent 18 | from msgraph_core.requests.batch_request_content_collection import BatchRequestContentCollection 19 | # Create a client 20 | # code to create graph client 21 | 22 | graph_client = GraphServiceClient(credentials=token, scopes=graph_scopes) 23 | 24 | # Create a request adapter from the client 25 | request_adapter = graph_client.request_adapter 26 | 27 | # Create some BatchRequestItems 28 | 29 | request_info1 = RequestInformation() 30 | request_info1.http_method = "GET" 31 | request_info1.url = "/me" 32 | request_info1.headers = RequestHeaders() 33 | request_info1.headers.add("Content-Type", "application/json") 34 | 35 | request_info2 = RequestInformation() 36 | request_info2.http_method = "GET" 37 | request_info2.url = "/users" 38 | request_info2.headers = RequestHeaders() 39 | request_info2.headers.add("Content-Type", "application/json") 40 | 41 | request_info3 = RequestInformation() 42 | request_info3.http_method = "GET" 43 | request_info3.url = "/me" 44 | request_info3.headers = RequestHeaders() 45 | request_info3.headers.add("Content-Type", "application/json") 46 | 47 | # Create BatchRequestItem instances 48 | batch_request_item1 = BatchRequestItem(request_information=request_info1) 49 | batch_request_item2 = BatchRequestItem(request_information=request_info2) 50 | 51 | # Add a request using RequestInformation directly 52 | batch_request_content.add_request_information(request_info1) 53 | print( 54 | f"Number of requests after adding request using RequestInformation: {len(batch_request_content.requests)}" 55 | ) 56 | print("------------------------------------------------------------------------------------") 57 | # Create an instance of BatchRequestContentCollection 58 | collection = BatchRequestContentCollection() 59 | # Add request items to the collection 60 | batch_request_item_to_add = BatchRequestItem(request_information=request_info3) 61 | batch_request_item_to_add1 = BatchRequestItem(request_information=request_info3) 62 | batch_request_item_to_add2 = BatchRequestItem(request_information=request_info3) 63 | batch_request_item_to_add3 = BatchRequestItem(request_information=request_info3) 64 | batch_request_item_to_add4 = BatchRequestItem(request_information=request_info3) 65 | 66 | collection.add_batch_request_item(batch_request_item_to_add) 67 | collection.add_batch_request_item(batch_request_item_to_add1) 68 | collection.add_batch_request_item(batch_request_item_to_add2) 69 | collection.add_batch_request_item(batch_request_item_to_add3) 70 | collection.add_batch_request_item(batch_request_item_to_add4) 71 | 72 | # Print the current batch requests 73 | print("Current Batch Requests:") 74 | for request in collection.current_batch.requests: 75 | print(f"Request ID: {request.id}, Status Code: {request.headers}") 76 | 77 | # Remove a request item from the collection 78 | collection.remove_batch_request_item(batch_request_item_to_add.id) 79 | print(f"Items left in the batch after removal: {len(collection.current_batch.requests)}") 80 | 81 | 82 | # post a collection 83 | async def main(): 84 | 85 | batch_response_content = await graph_client.batch.post(batch_request_content=collection) 86 | responses = batch_response_content.get_responses() 87 | for item in responses: 88 | for item_body in item.responses: 89 | print(f"Item: {item_body.id}, Status Code: {item_body.status}") 90 | print(f"body: {item_body.body} Item headers: {item_body.headers} ") 91 | print("-----------------------------------------------") 92 | 93 | 94 | # Run the main function 95 | asyncio.run(main()) 96 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_request_with_content.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the Batch request with content""" 7 | import asyncio 8 | 9 | from kiota_abstractions.request_information import RequestInformation 10 | from kiota_abstractions.method import Method 11 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 12 | 13 | from msgraph_core.requests.batch_request_item import BatchRequestItem 14 | from msgraph_core.requests.batch_request_content import BatchRequestContent 15 | from msgraph_core.requests.batch_request_content_collection import BatchRequestContentCollection 16 | 17 | from msgraph_core.requests.batch_response_content import BatchResponseContent 18 | from msgraph_core.requests.batch_response_content_collection import BatchResponseContentCollection 19 | 20 | # Create a client 21 | # code to create a graph client 22 | graph_client = GraphServiceClient(credentials=token, scopes=graph_scopes) 23 | 24 | # Create a request adapter from the clinet 25 | request_adapter = graph_client.request_adapter 26 | 27 | # Create batch Items 28 | request_info1 = RequestInformation() 29 | request_info1.http_method = "GET" 30 | request_info1.url = "https://graph.microsoft.com/v1.0/me" 31 | request_info1.url = "/me" 32 | 33 | request_info1.headers = RequestHeaders() 34 | request_info1.headers.add("Content-Type", "application/json") 35 | 36 | request_info2 = RequestInformation() 37 | request_info2.http_method = "GET" 38 | request_info2.url = "/users" 39 | request_info2.headers = RequestHeaders() 40 | request_info2.headers.add("Content-Type", "application/json") 41 | 42 | batch_request_item1 = BatchRequestItem(request_information=request_info1) 43 | batch_request_item2 = BatchRequestItem(request_information=request_info2) 44 | 45 | # Create a batch request content 46 | batch_request_content = { 47 | batch_request_item1.id: batch_request_item1, 48 | batch_request_item2.id: batch_request_item2 49 | } 50 | batch_content = BatchRequestContent(batch_request_content) 51 | 52 | 53 | async def main(): 54 | batch_response_content = await graph_client.batch.post(batch_request_content=batch_content) 55 | 56 | # Print the batch response content 57 | print(f"Batch Response Content: {batch_response_content.responses}") 58 | for response in batch_response_content.responses: 59 | print(f"Request ID: {response.id}, Status: {response.status}") 60 | print(f"Response body: {response.body}, headers: {response.headers}") 61 | print("-------------------------------------------------------------") 62 | 63 | 64 | asyncio.run(main()) 65 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_request_with_custom_error_class.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the Batch request with Custom Error Class""" 7 | 8 | import asyncio 9 | 10 | from kiota_abstractions.request_adapter import RequestAdapter 11 | from kiota_abstractions.serialization import Parsable 12 | from kiota_abstractions.request_information import RequestInformation 13 | from kiota_abstractions.method import Method 14 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 15 | 16 | from msgraph import GraphServiceClient 17 | 18 | from msgraph_core.requests.batch_request_item import BatchRequestItem 19 | from msgraph_core.requests.batch_request_content import BatchRequestContent 20 | 21 | from msgraph_core.requests.batch_response_content import BatchResponseContent 22 | # create client 23 | # code to create client 24 | graph_client = GraphServiceClient(credentials=token, scopes=graph_scopes) 25 | 26 | 27 | # Create an Error map Parsable or import it from wherever you have it 28 | class CustomError(Parsable): 29 | 30 | def __init__(self) -> None: 31 | self.error_code: str = None 32 | self.message: str = None 33 | 34 | @staticmethod 35 | def not_found() -> 'CustomError': 36 | error = CustomError() 37 | error.error_code = "404" 38 | error.message = "Resource not found" 39 | return error 40 | 41 | 42 | # Create a request adapter from client 43 | request_adapter = graph_client.request_adapter 44 | 45 | # Create batch Items 46 | request_info1 = RequestInformation() 47 | request_info1.http_method = "GET" 48 | request_info1.url = "https://graph.microsoft.com/v1.0/me" 49 | request_info1.url = "/me" 50 | 51 | request_info1.headers = RequestHeaders() 52 | request_info1.headers.add("Content-Type", "application/json") 53 | 54 | request_info2 = RequestInformation() 55 | request_info2.http_method = "GET" 56 | request_info2.url = "/users" 57 | request_info2.headers = RequestHeaders() 58 | request_info2.headers.add("Content-Type", "application/json") 59 | 60 | # get user who does not exist to test 404 in error map 61 | request_info3 = RequestInformation() 62 | request_info3.http_method = "GET" 63 | request_info3.url = "/users/random-id" 64 | request_info3.headers = RequestHeaders() 65 | request_info3.headers.add("Content-Type", "application/json") 66 | 67 | # bacth request items to be added to content 68 | batch_request_item1 = BatchRequestItem(request_information=request_info1) 69 | batch_request_item2 = BatchRequestItem(request_information=request_info2) 70 | batch_request_item3 = BatchRequestItem(request_information=request_info3) 71 | 72 | # Create a BatchRequestContent 73 | batch_request_content = { 74 | batch_request_item1.id: batch_request_item1, 75 | batch_request_item2.id: batch_request_item2 76 | } 77 | 78 | batch_content = BatchRequestContent(batch_request_content) 79 | 80 | 81 | # Function to demonstrate the usage of BatchRequestBuilder 82 | async def main(): 83 | error_map = {"400": CustomError, "404": CustomError.not_found} 84 | 85 | batch_response_content = await graph_client.batch.post( 86 | batch_request_content=batch_content, error_map=error_map 87 | ) 88 | 89 | # Print the batch response content 90 | print(f"Batch Response Content: {batch_response_content.responses}") 91 | for response in batch_response_content.responses: 92 | print(f"Request ID: {response.id}, Status: {response.status}") 93 | print(f"Response body: {response.body}, headers: {response.headers}") 94 | print("-------------------------------------------------------------") 95 | 96 | 97 | asyncio.run(main()) 98 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_request_with_parsable_as_response_type.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the Batch request with Parsable Resposnse Type""" 7 | import asyncio 8 | 9 | from kiota_abstractions.request_adapter import RequestAdapter 10 | from kiota_abstractions.request_information import RequestInformation 11 | from kiota_abstractions.method import Method 12 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 13 | 14 | from msgraph_core.requests.batch_request_item import BatchRequestItem 15 | from msgraph_core.requests.batch_request_content import BatchRequestContent 16 | 17 | # import User model to serialize to 18 | from msgraph.generated.models.user import User 19 | # Create a client 20 | # code to create graph client 21 | graph_client = GraphServiceClient(credentials=token, scopes=graph_scopes) 22 | 23 | print(f"Graph Scopes: {graph_scopes}") 24 | 25 | # Create a request adapter from the client 26 | request_adapter = graph_client.request_adapter 27 | 28 | # Create batch Items 29 | request_info1 = RequestInformation() 30 | request_info1.http_method = "GET" 31 | request_info1.url = "/users/" 32 | 33 | request_info1.headers = RequestHeaders() 34 | request_info1.headers.add("Content-Type", "application/json") 35 | 36 | request_info2 = RequestInformation() 37 | request_info2.http_method = "GET" 38 | request_info2.url = "/users/" 39 | request_info2.headers = RequestHeaders() 40 | request_info2.headers.add("Content-Type", "application/json") 41 | 42 | # bacth request items to be added to content 43 | batch_request_item1 = BatchRequestItem(request_information=request_info1) 44 | batch_request_item2 = BatchRequestItem(request_information=request_info2) 45 | 46 | # Create a batch request content 47 | batch_request_content = { 48 | batch_request_item1.id: batch_request_item1, 49 | batch_request_item2.id: batch_request_item2 50 | } 51 | 52 | batch_content = BatchRequestContent(batch_request_content) 53 | 54 | 55 | # Function to demonstrate the usage of BatchRequestBuilder 56 | async def main(): 57 | batch_response_content = await graph_client.batch.post(batch_request_content=batch_content) 58 | # response_type=User 59 | 60 | try: 61 | individual_response = batch_response_content.get_response_by_id( 62 | batch_request_item1.id, User 63 | ) 64 | print(f"Individual Response: {individual_response}") 65 | except AttributeError as e: 66 | print(f"Error getting response by ID: {e}") 67 | 68 | 69 | asyncio.run(main()) 70 | -------------------------------------------------------------------------------- /samples/batch_requests/batch_response_get_status_codes.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates getting status codes in Batch Responses""" 7 | batch_request_content = { 8 | batch_request_item1.id: batch_request_item1, 9 | batch_request_item2.id: batch_request_item2 10 | } 11 | 12 | batch_content = BatchRequestContent(batch_request_content) 13 | 14 | 15 | async def main(): 16 | batch_response_content = await client.batch.post(batch_request_content=batch_content) 17 | 18 | try: 19 | status_codes = batch_response_content.get_response_status_codes() 20 | print(f"Status Codes: {status_codes}") 21 | except AttributeError as e: 22 | print(f"Error getting respons status codes: {e}") 23 | 24 | 25 | asyncio.run(main()) 26 | -------------------------------------------------------------------------------- /samples/client_factory_samples.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """ 7 | Demonstrates using the HTTPClientFactory to create a client and make HTTP requests 8 | to Microsoft Graph 9 | """ 10 | import json 11 | from pprint import pprint 12 | 13 | # This sample uses InteractiveBrowserCredential only for demonstration. 14 | # Any azure-identity TokenCredential class will work the same. 15 | from azure.identity import InteractiveBrowserCredential 16 | from msgraph.core import APIVersion, HTTPClientFactory, NationalClouds 17 | from requests import Session 18 | 19 | scopes = ['user.read'] 20 | browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') 21 | 22 | # Create client with default middleware 23 | client = HTTPClientFactory().create_with_default_middleware(browser_credential) 24 | 25 | 26 | def get_sample(): 27 | """Sample HTTP GET request using the client""" 28 | result = client.get( 29 | '/users', 30 | params={ 31 | '$select': 'displayName', 32 | '$top': '10' 33 | }, 34 | ) 35 | pprint(result.json()) 36 | 37 | 38 | def post_sample(): 39 | """Sample HTTP POST request using the client""" 40 | body = { 41 | 'message': { 42 | 'subject': 'Python SDK Meet for lunch?', 43 | 'body': { 44 | 'contentType': 'Text', 45 | 'content': 'The new cafeteria is open.' 46 | }, 47 | 'toRecipients': [{ 48 | 'emailAddress': { 49 | 'address': 'ENTER_RECEPIENT_EMAIL_ADDRESS' 50 | } 51 | }] 52 | } 53 | } 54 | 55 | result = client \ 56 | .post('/me/sendMail', 57 | data=json.dumps(body), 58 | scopes=['mail.send'], 59 | headers={'Content-Type': 'application/json'} 60 | ) 61 | pprint(result.status_code) 62 | 63 | 64 | def client_with_custom_session_sample(): 65 | """Sample client with a custom Session object""" 66 | session = Session() 67 | my_client = HTTPClientFactory(session=session 68 | ).create_with_default_middleware(browser_credential) 69 | result = my_client.get('/me') 70 | pprint(result.json()) 71 | 72 | 73 | def client_with_custom_settings_sample(): 74 | """Sample client that makes requests against the beta api on a specified cloud endpoint""" 75 | my_client = HTTPClientFactory( 76 | credential=browser_credential, 77 | api_version=APIVersion.beta, 78 | cloud=NationalClouds.Germany, 79 | ).create_with_default_middleware(browser_credential) 80 | result = my_client.get( 81 | '/users', 82 | params={ 83 | '$select': 'displayName', 84 | '$top': '10' 85 | }, 86 | ) 87 | pprint(result.json()) 88 | 89 | 90 | def client_with_custom_middleware(): 91 | """Sample client with a custom middleware chain""" 92 | middleware = [ 93 | CustomAuthorizationHandler(), 94 | MyCustomMiddleware(), 95 | ] 96 | 97 | my_client = HTTPClientFactory().create_with_custom_middleware(middleware) 98 | result = my_client.get( 99 | 'https://graph.microsoft.com/v1.0/users', 100 | params={ 101 | '$select': 'displayName', 102 | '$top': '10' 103 | }, 104 | ) 105 | pprint(result.json()) 106 | 107 | 108 | if __name__ == '__main__': 109 | get_sample() 110 | post_sample() 111 | client_with_custom_session_sample() 112 | client_with_custom_settings_sample() 113 | client_with_custom_middleware() 114 | -------------------------------------------------------------------------------- /samples/graph_client_samples.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the GraphClient to make HTTP Requests to Microsoft Graph""" 7 | import json 8 | from pprint import pprint 9 | 10 | # This sample uses InteractiveBrowserCredential only for demonstration. 11 | # Any azure-identity TokenCredential class will work the same. 12 | from azure.identity import InteractiveBrowserCredential 13 | from msgraph.core import APIVersion, GraphClient, NationalClouds 14 | from requests import Session 15 | 16 | scopes = ['user.read'] 17 | browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') 18 | client = GraphClient(credential=browser_credential) 19 | 20 | 21 | def get_sample(): 22 | """Sample HTTP GET request using the GraphClient""" 23 | result = client.get('/me/messages', scopes=['mail.read']) 24 | pprint(result.json()) 25 | 26 | 27 | def post_sample(): 28 | """Sample HTTP POST request using the GraphClient""" 29 | body = { 30 | 'message': { 31 | 'subject': 'Python SDK Meet for lunch?', 32 | 'body': { 33 | 'contentType': 'Text', 34 | 'content': 'The new cafeteria is open.' 35 | }, 36 | 'toRecipients': [{ 37 | 'emailAddress': { 38 | 'address': 'ENTER_RECEPIENT_EMAIL_ADDRESS' 39 | } 40 | }] 41 | } 42 | } 43 | 44 | result = client \ 45 | .post('/me/sendMail', 46 | data=json.dumps(body), 47 | scopes=['mail.send'], 48 | headers={'Content-Type': 'application/json'} 49 | ) 50 | pprint(result.status_code) 51 | 52 | 53 | def client_with_custom_session_sample(): 54 | """Sample client with a custom Session object""" 55 | session = Session() 56 | my_client = GraphClient(credential=browser_credential, session=session) 57 | result = my_client.get('/me') 58 | pprint(result.json()) 59 | 60 | 61 | def client_with_custom_settings_sample(): 62 | """ 63 | Sample client that makes requests against the beta api on a specified cloud endpoint 64 | """ 65 | my_client = GraphClient( 66 | credential=browser_credential, 67 | api_version=APIVersion.beta, 68 | cloud=NationalClouds.Germany, 69 | ) 70 | result = my_client.get( 71 | '/users', 72 | params={ 73 | '$select': 'displayName', 74 | '$top': '10' 75 | }, 76 | ) 77 | pprint(result.json()) 78 | 79 | 80 | def client_with_custom_middleware(): 81 | """Sample client with a custom middleware chain""" 82 | middleware = [ 83 | CustomAuthorizationHandler(), 84 | MyCustomMiddleware(), 85 | ] 86 | 87 | my_client = GraphClient(credential=browser_credential, middleware=middleware) 88 | result = my_client.get( 89 | 'https://graph.microsoft.com/v1.0/users', 90 | params={ 91 | '$select': 'displayName', 92 | '$top': '10' 93 | }, 94 | ) 95 | pprint(result.json()) 96 | 97 | 98 | if __name__ == '__main__': 99 | post_sample() 100 | get_sample() 101 | client_with_custom_session_sample() 102 | client_with_custom_settings_sample() 103 | client_with_custom_middleware() 104 | -------------------------------------------------------------------------------- /samples/retry_handler_samples.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=undefined-variable 6 | """Demonstrates using the GraphClient to make HTTP Requests to Microsoft Graph""" 7 | import json 8 | from pprint import pprint 9 | 10 | from azure.identity import InteractiveBrowserCredential 11 | from msgraph.core import GraphClient, HTTPClientFactory 12 | 13 | scopes = ['user.read'] 14 | # This sample uses InteractiveBrowserCredential only for demonstration. 15 | # Any azure-identity TokenCredential class will work the same. 16 | browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID') 17 | 18 | 19 | def sample_http_client_with_custom_retry_defaults(): 20 | """ 21 | Initializing a sample client with default middleware using the HTTPClient and passing 22 | default configs to the retryhandler. These defaults will be used for every subsequent 23 | request using the client.""" 24 | 25 | client = HTTPClientFactory().create_with_default_middleware( 26 | browser_credential, max_retries=5, retry_backoff_factor=0.1, retry_time_limit=60 27 | ) 28 | result = client.get('/me/messages', scopes=['mail.read']) 29 | pprint(result.json()) 30 | 31 | 32 | def sample_graph_client_with_custom_retry_defaults(): 33 | """Initializing a sample graph client and passing default configs to the default retry 34 | handler. These defaults will be used for every subsequent request using the client unless 35 | per request options are passed""" 36 | 37 | client = GraphClient(credential=browser_credential, max_retries=2, retry_backoff_factor=0.5) 38 | result = client.get('/me/messages', scopes=['mail.read']) 39 | pprint(result.json()) 40 | 41 | 42 | def sample_graph_client_with_per_request_retry_options(): 43 | """Sending a request using the graph client with retry options for that specific request. 44 | This will override the default config for the retry handler""" 45 | 46 | client = GraphClient(credential=browser_credential) 47 | result = client.get( 48 | '/me/messages', scopes=['mail.read'], retry_on_status_codes=[429, 502, 503, 504] 49 | ) 50 | pprint(result.json()) 51 | -------------------------------------------------------------------------------- /src/msgraph_core/__init__.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ----------------------------------- 5 | 6 | # pylint: disable=line-too-long 7 | # This is to allow complete package description on PyPI 8 | """ 9 | Core component of the Microsoft Graph Python SDK consisting of HTTP/Graph Client and a configurable middleware pipeline (Preview). 10 | """ 11 | from ._constants import SDK_VERSION 12 | from ._enums import APIVersion, NationalClouds 13 | from .authentication import AzureIdentityAuthenticationProvider 14 | from .base_graph_request_adapter import BaseGraphRequestAdapter 15 | from .graph_client_factory import GraphClientFactory 16 | from .models import PageResult 17 | from .tasks import PageIterator 18 | 19 | __version__ = SDK_VERSION 20 | -------------------------------------------------------------------------------- /src/msgraph_core/_constants.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | """ 6 | Application constants. All defaults can be changed when initializing a client 7 | via the GraphClient or HttpClientFactory 8 | """ 9 | DEFAULT_REQUEST_TIMEOUT = 100 10 | DEFAULT_CONNECTION_TIMEOUT = 30 11 | # The SDK version 12 | # x-release-please-start-version 13 | SDK_VERSION = '1.3.4' 14 | # x-release-please-end 15 | MS_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default' 16 | -------------------------------------------------------------------------------- /src/msgraph_core/_enums.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | #pylint: disable=invalid-name 6 | 7 | from enum import Enum 8 | 9 | 10 | class APIVersion(str, Enum): 11 | """Enumerated list of supported API Versions""" 12 | beta = 'beta' 13 | v1 = 'v1.0' 14 | 15 | def __str__(self): 16 | return self.value 17 | 18 | 19 | class FeatureUsageFlag(int, Enum): 20 | """Enumerated list of values used to flag usage of specific middleware""" 21 | 22 | NONE = 0 23 | REDIRECT_HANDLER_ENABLED = 1 24 | RETRY_HANDLER_ENABLED = 2 25 | AUTH_HANDLER_ENABLED = 4 26 | DEFAULT_HTTP_PROVIDER_ENABLED = 8 27 | LOGGING_HANDLER_ENABLED = 16 28 | 29 | def __str__(self): 30 | return self.value 31 | 32 | 33 | class NationalClouds(str, Enum): 34 | """Enumerated list of supported sovereign clouds""" 35 | 36 | China = 'https://microsoftgraph.chinacloudapi.cn' 37 | Germany = 'https://graph.microsoft.de' 38 | Global = 'https://graph.microsoft.com' 39 | US_DoD = 'https://dod-graph.microsoft.us' 40 | US_GOV = 'https://graph.microsoft.us' 41 | 42 | def __str__(self): 43 | return self.value 44 | -------------------------------------------------------------------------------- /src/msgraph_core/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from .azure_identity_authentication_provider import AzureIdentityAuthenticationProvider 2 | 3 | __all__ = ['AzureIdentityAuthenticationProvider'] 4 | -------------------------------------------------------------------------------- /src/msgraph_core/authentication/azure_identity_authentication_provider.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional, Union 2 | 3 | from kiota_authentication_azure.azure_identity_authentication_provider import ( 4 | AzureIdentityAuthenticationProvider as KiotaAzureIdentityAuthenticationProvider, 5 | ) 6 | 7 | from msgraph_core._constants import MS_DEFAULT_SCOPE 8 | from msgraph_core._enums import NationalClouds 9 | 10 | if TYPE_CHECKING: 11 | from azure.core.credentials import TokenCredential 12 | from azure.core.credentials_async import AsyncTokenCredential 13 | 14 | 15 | class AzureIdentityAuthenticationProvider(KiotaAzureIdentityAuthenticationProvider): 16 | 17 | def __init__( 18 | self, 19 | credentials: Union["TokenCredential", "AsyncTokenCredential"], 20 | options: Optional[dict] = {}, 21 | scopes: list[str] = [], 22 | allowed_hosts: list[str] = [nc.value for nc in NationalClouds] 23 | ) -> None: 24 | """[summary] 25 | 26 | Args: 27 | credentials (Union["TokenCredential", "AsyncTokenCredential"]): The 28 | tokenCredential implementation to use for authentication. 29 | options (Optional[dict]): The options to use for authentication. 30 | scopes (list[str]): The scopes to use for authentication. 31 | Defaults to 'https:///.default'. 32 | allowed_hosts (Optional[list[str]]): The allowed hosts to use for 33 | authentication. Defaults to Microsoft National Clouds. 34 | """ 35 | super().__init__(credentials, options, scopes, allowed_hosts) 36 | -------------------------------------------------------------------------------- /src/msgraph_core/base_graph_request_adapter.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import httpx 4 | from kiota_abstractions.authentication import AuthenticationProvider 5 | from kiota_abstractions.serialization import ( 6 | ParseNodeFactory, 7 | ParseNodeFactoryRegistry, 8 | SerializationWriterFactory, 9 | SerializationWriterFactoryRegistry, 10 | ) 11 | from kiota_http.httpx_request_adapter import HttpxRequestAdapter 12 | 13 | from .graph_client_factory import GraphClientFactory 14 | 15 | 16 | class BaseGraphRequestAdapter(HttpxRequestAdapter): 17 | 18 | def __init__( 19 | self, 20 | authentication_provider: AuthenticationProvider, 21 | parse_node_factory: Optional[ParseNodeFactory] = None, 22 | serialization_writer_factory: Optional[SerializationWriterFactory] = None, 23 | http_client: Optional[httpx.AsyncClient] = None 24 | ) -> None: 25 | if parse_node_factory is None: 26 | parse_node_factory = ParseNodeFactoryRegistry() 27 | if serialization_writer_factory is None: 28 | serialization_writer_factory = SerializationWriterFactoryRegistry() 29 | if http_client is None: 30 | http_client = GraphClientFactory.create_with_default_middleware() 31 | super().__init__( 32 | authentication_provider=authentication_provider, 33 | parse_node_factory=parse_node_factory, 34 | serialization_writer_factory=serialization_writer_factory, 35 | http_client=http_client 36 | ) 37 | -------------------------------------------------------------------------------- /src/msgraph_core/graph_client_factory.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | from __future__ import annotations 6 | 7 | from typing import Optional 8 | 9 | import httpx 10 | from kiota_abstractions.request_option import RequestOption 11 | from kiota_http.kiota_client_factory import KiotaClientFactory 12 | from kiota_http.middleware.middleware import BaseMiddleware 13 | 14 | from ._enums import APIVersion, NationalClouds 15 | from .middleware import AsyncGraphTransport, GraphTelemetryHandler 16 | from .middleware.options import GraphTelemetryHandlerOption 17 | 18 | 19 | class GraphClientFactory(KiotaClientFactory): 20 | """Constructs httpx.AsyncClient instances configured with either custom or default 21 | pipeline of graph specific middleware. 22 | """ 23 | 24 | @staticmethod 25 | def create_with_default_middleware( # type: ignore 26 | # Breaking change to remove KiotaClientFactory as base class 27 | api_version: APIVersion = APIVersion.v1, 28 | client: Optional[httpx.AsyncClient] = None, 29 | host: NationalClouds = NationalClouds.Global, 30 | options: Optional[dict[str, RequestOption]] = None 31 | ) -> httpx.AsyncClient: 32 | """Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with 33 | a custom transport loaded with a default pipeline of middleware. 34 | 35 | Args: 36 | api_version (APIVersion): The Graph API version to be used. 37 | Defaults to APIVersion.v1. 38 | client (httpx.AsyncClient): The httpx.AsyncClient instance to be used. 39 | Defaults to KiotaClientFactory.get_default_client(). 40 | host (NationalClouds): The national clound endpoint to be used. 41 | Defaults to NationalClouds.Global. 42 | options (Optional[dict[str, RequestOption]]): The request options to use when 43 | instantiating default middleware. Defaults to dict[str, RequestOption]=None. 44 | 45 | Returns: 46 | httpx.AsyncClient: An instance of the AsyncClient object 47 | """ 48 | if client is None: 49 | client = KiotaClientFactory.get_default_client() 50 | client.base_url = GraphClientFactory._get_base_url(host, api_version) # type: ignore 51 | middleware = KiotaClientFactory.get_default_middleware(options) 52 | telemetry_handler = GraphClientFactory._get_telemetry_handler(options) 53 | middleware.append(telemetry_handler) 54 | return GraphClientFactory._load_middleware_to_client(client, middleware) 55 | 56 | @staticmethod 57 | def create_with_custom_middleware( # type: ignore 58 | # Breaking change to remove Kiota client factory as base class 59 | middleware: Optional[list[BaseMiddleware]], 60 | api_version: APIVersion = APIVersion.v1, 61 | client: Optional[httpx.AsyncClient] = None, 62 | host: NationalClouds = NationalClouds.Global, 63 | ) -> httpx.AsyncClient: 64 | """Applies a custom middleware chain to the HTTP Client 65 | 66 | Args: 67 | middleware(list[BaseMiddleware]): Custom middleware list that will be used to create 68 | a middleware pipeline. The middleware should be arranged in the order in which they will 69 | modify the request. 70 | api_version (APIVersion): The Graph API version to be used. 71 | Defaults to APIVersion.v1. 72 | client (httpx.AsyncClient): The httpx.AsyncClient instance to be used. 73 | Defaults to KiotaClientFactory.get_default_client(). 74 | host (NationalClouds): The national clound endpoint to be used. 75 | Defaults to NationalClouds.Global. 76 | """ 77 | if client is None: 78 | client = KiotaClientFactory.get_default_client() 79 | client.base_url = GraphClientFactory._get_base_url(host, api_version) # type: ignore 80 | return GraphClientFactory._load_middleware_to_client(client, middleware) 81 | 82 | @staticmethod 83 | def _get_base_url(host: str, api_version: APIVersion) -> str: 84 | """Helper method to set the complete base url""" 85 | base_url = f'{host}/{api_version}' 86 | return base_url 87 | 88 | @staticmethod 89 | def _get_telemetry_handler( 90 | options: Optional[dict[str, RequestOption]] 91 | ) -> GraphTelemetryHandler: 92 | """Helper method to get the graph telemetry handler instantiated with appropriate 93 | options""" 94 | 95 | if options: 96 | graph_telemetry_options: GraphTelemetryHandlerOption = options.get( 97 | GraphTelemetryHandlerOption().get_key() 98 | ) # type: ignore # Unable to down cast type 99 | if graph_telemetry_options: 100 | return GraphTelemetryHandler(options=graph_telemetry_options) 101 | return GraphTelemetryHandler() 102 | 103 | @staticmethod 104 | def _load_middleware_to_client( 105 | client: httpx.AsyncClient, middleware: Optional[list[BaseMiddleware]] 106 | ) -> httpx.AsyncClient: 107 | current_transport = client._transport 108 | client._transport = GraphClientFactory._replace_transport_with_custom_graph_transport( 109 | current_transport, middleware 110 | ) 111 | if client._mounts: 112 | mounts: dict = {} 113 | for pattern, transport in client._mounts.items(): 114 | if transport is None: 115 | mounts[pattern] = None 116 | else: 117 | mounts[pattern 118 | ] = GraphClientFactory._replace_transport_with_custom_graph_transport( 119 | transport, middleware 120 | ) 121 | client._mounts = dict(sorted(mounts.items())) 122 | return client 123 | 124 | @staticmethod 125 | def _replace_transport_with_custom_graph_transport( 126 | current_transport: httpx.AsyncBaseTransport, middleware: Optional[list[BaseMiddleware]] 127 | ) -> AsyncGraphTransport: 128 | middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( 129 | middleware, current_transport 130 | ) 131 | new_transport = AsyncGraphTransport( 132 | transport=current_transport, pipeline=middleware_pipeline 133 | ) 134 | return new_transport 135 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | from .async_graph_transport import AsyncGraphTransport 6 | from .request_context import GraphRequestContext 7 | from .telemetry import GraphTelemetryHandler 8 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/async_graph_transport.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import httpx 4 | from kiota_http.middleware import MiddlewarePipeline, RedirectHandler, RetryHandler 5 | 6 | from .._enums import FeatureUsageFlag 7 | from .request_context import GraphRequestContext 8 | 9 | 10 | class AsyncGraphTransport(httpx.AsyncBaseTransport): 11 | """A custom transport for requests to the Microsoft Graph API 12 | """ 13 | 14 | def __init__(self, transport: httpx.AsyncBaseTransport, pipeline: MiddlewarePipeline) -> None: 15 | self.transport = transport 16 | self.pipeline = pipeline 17 | 18 | async def handle_async_request(self, request: httpx.Request) -> httpx.Response: 19 | if self.pipeline and hasattr(request, 'options'): 20 | self.set_request_context_and_feature_usage(request) 21 | response = await self.pipeline.send(request) 22 | return response 23 | 24 | response = await self.transport.handle_async_request(request) 25 | return response 26 | 27 | def set_request_context_and_feature_usage(self, request: httpx.Request) -> httpx.Request: 28 | 29 | request_options = request.options # type:ignore 30 | 31 | context = GraphRequestContext(request_options, request.headers) 32 | middleware = self.pipeline._first_middleware 33 | while middleware: 34 | if isinstance(middleware, RedirectHandler): 35 | context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED 36 | if isinstance(middleware, RetryHandler): 37 | context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED 38 | 39 | middleware = middleware.next 40 | request.context = context #type: ignore 41 | return request 42 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/options/__init__.py: -------------------------------------------------------------------------------- 1 | from .graph_telemetry_handler_option import GraphTelemetryHandlerOption 2 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/options/graph_telemetry_handler_option.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from kiota_abstractions.request_option import RequestOption 4 | 5 | from ..._constants import SDK_VERSION 6 | from ..._enums import APIVersion 7 | 8 | 9 | class GraphTelemetryHandlerOption(RequestOption): 10 | """Config options for the GraphTelemetryHandler 11 | """ 12 | 13 | GRAPH_TELEMETRY_HANDLER_OPTION_KEY = "GraphTelemetryHandlerOption" 14 | 15 | def __init__( 16 | self, api_version: Optional[APIVersion] = None, sdk_version: str = SDK_VERSION 17 | ) -> None: 18 | """To create an instance of GraphTelemetryHandlerOption 19 | 20 | Args: 21 | api_version (Optional[APIVersion], optional): The Graph API version in use. 22 | Defaults to None. 23 | sdk_version (str): The sdk version in use. 24 | Defaults to SDK_VERSION of grap core. 25 | """ 26 | self._api_version = api_version 27 | self._sdk_version = sdk_version 28 | 29 | @property 30 | def api_version(self): 31 | """The Graph API version in use""" 32 | return self._api_version 33 | 34 | @api_version.setter 35 | def api_version(self, value: APIVersion): 36 | self._api_version = value 37 | 38 | @property 39 | def sdk_version(self): 40 | """The sdk version in use""" 41 | return self._sdk_version 42 | 43 | @sdk_version.setter 44 | def sdk_version(self, value: str): 45 | self._sdk_version = value 46 | 47 | @staticmethod 48 | def get_key() -> str: 49 | return GraphTelemetryHandlerOption.GRAPH_TELEMETRY_HANDLER_OPTION_KEY 50 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | import uuid 6 | 7 | import httpx 8 | 9 | from .._enums import FeatureUsageFlag 10 | 11 | 12 | class GraphRequestContext: 13 | """A request context contains data that is persisted throughout the request and 14 | includes a ClientRequestId property, MiddlewareControl property to control behavior 15 | of middleware as well as a FeatureUsage property to keep track of middleware used 16 | in making the request. 17 | """ 18 | 19 | def __init__(self, middleware_control: dict, headers: httpx.Headers): 20 | """Constructor for request context instances 21 | 22 | Args: 23 | middleware_control (dict): A dictionary of optional middleware options 24 | that can be accessed by middleware components to override the options provided 25 | during middleware initialization, 26 | 27 | headers (dict): A dictionary containing the request headers. Used to check for a 28 | user provided client request id. 29 | """ 30 | self.middleware_control = middleware_control 31 | self.client_request_id = headers.get('client-request-id', str(uuid.uuid4())) 32 | self._feature_usage: int = FeatureUsageFlag.NONE 33 | 34 | @property 35 | def feature_usage(self): 36 | return hex(self._feature_usage) 37 | 38 | @feature_usage.setter 39 | def feature_usage(self, flag: FeatureUsageFlag) -> None: 40 | self._feature_usage = self._feature_usage | flag 41 | -------------------------------------------------------------------------------- /src/msgraph_core/middleware/telemetry.py: -------------------------------------------------------------------------------- 1 | import http 2 | import json 3 | import platform 4 | 5 | import httpx 6 | from kiota_http.middleware import BaseMiddleware 7 | from urllib3.util import parse_url 8 | 9 | from .._constants import SDK_VERSION 10 | from .._enums import APIVersion, NationalClouds 11 | from .async_graph_transport import AsyncGraphTransport 12 | from .options import GraphTelemetryHandlerOption 13 | from .request_context import GraphRequestContext 14 | 15 | 16 | class GraphRequest(httpx.Request): 17 | context: GraphRequestContext 18 | 19 | 20 | class GraphTelemetryHandler(BaseMiddleware): 21 | """Middleware component that attaches metadata to a Graph request in order to help 22 | the SDK team improve the developer experience. 23 | """ 24 | 25 | def __init__( 26 | self, options: GraphTelemetryHandlerOption = GraphTelemetryHandlerOption(), **kwargs 27 | ): 28 | """Create an instance of GraphTelemetryHandler 29 | 30 | Args: 31 | options (GraphTelemetryHandlerOption, optional): The graph telemetry handler 32 | options value. Defaults to GraphTelemetryHandlerOption 33 | """ 34 | super().__init__() 35 | self.options = options 36 | 37 | async def send(self, request: GraphRequest, transport: AsyncGraphTransport): 38 | """Adds telemetry headers and sends the http request. 39 | """ 40 | current_options = self._get_current_options(request) 41 | 42 | if self.is_graph_url(request.url): 43 | self._add_client_request_id_header(request) 44 | self._append_sdk_version_header(request, current_options) 45 | self._add_host_os_header(request) 46 | self._add_runtime_environment_header(request) 47 | 48 | response = await super().send(request, transport) 49 | return response 50 | 51 | def _get_current_options(self, request: httpx.Request) -> GraphTelemetryHandlerOption: 52 | """Returns the options to use for the request.Overries default options if 53 | request options are passed. 54 | 55 | Args: 56 | request (httpx.Request): The prepared request object 57 | 58 | Returns: 59 | GraphTelemetryHandlerOption: The options to used. 60 | """ 61 | current_options = self.options 62 | request_options = request.context.middleware_control.get( # type:ignore 63 | GraphTelemetryHandlerOption.get_key() 64 | ) 65 | # Override default options with request options 66 | if request_options: 67 | current_options = request_options 68 | 69 | return current_options 70 | 71 | def is_graph_url(self, url): 72 | """Check if the request is made to a graph endpoint. We do not add telemetry headers to 73 | non-graph endpoints""" 74 | endpoints = set(item.value for item in NationalClouds) 75 | 76 | base_url = parse_url(str(url)) 77 | endpoint = f"{base_url.scheme}://{base_url.netloc}" 78 | return endpoint in endpoints 79 | 80 | def _add_client_request_id_header(self, request) -> None: 81 | """Add a client-request-id header with GUID value to request""" 82 | request.headers.update({'client-request-id': f'{request.context.client_request_id}'}) 83 | 84 | def _append_sdk_version_header(self, request, options) -> None: 85 | """Add SdkVersion request header to each request to identify the language and 86 | version of the client SDK library(s). 87 | Also adds the featureUsage value. 88 | """ 89 | core_library_name = f'graph-python-core/{SDK_VERSION}' 90 | service_lib_name = '' 91 | 92 | if options.api_version == APIVersion.v1: 93 | service_lib_name = f'graph-python/{options.sdk_version}' 94 | if options.api_version == APIVersion.beta: 95 | service_lib_name = f'graph-python-beta/{options.sdk_version}' 96 | 97 | if service_lib_name: 98 | telemetry_header_string = f'{service_lib_name}, '\ 99 | f'{core_library_name} (featureUsage={request.context.feature_usage})' 100 | else: 101 | telemetry_header_string = f'{core_library_name} '\ 102 | '(featureUsage={request.context.feature_usage})' 103 | 104 | if 'sdkVersion' in request.headers: 105 | sdk_version = request.headers.get('sdkVersion') 106 | if not sdk_version == telemetry_header_string: 107 | request.headers.update({'sdkVersion': telemetry_header_string}) 108 | else: 109 | request.headers.update({'sdkVersion': telemetry_header_string}) 110 | 111 | def _add_host_os_header(self, request) -> None: 112 | """ 113 | Add HostOS request header to each request to help identify the OS 114 | on which our client SDK is running on 115 | """ 116 | system = platform.system() 117 | version = platform.version() 118 | host_os = f'{system} {version}' 119 | request.headers.update({'HostOs': host_os}) 120 | 121 | def _add_runtime_environment_header(self, request) -> None: 122 | """ 123 | Add RuntimeEnvironment request header to capture the runtime framework 124 | on which the client SDK is running on. 125 | """ 126 | python_version = platform.python_version() 127 | runtime_environment = f'Python/{python_version}' 128 | request.headers.update({'RuntimeEnvironment': runtime_environment}) 129 | -------------------------------------------------------------------------------- /src/msgraph_core/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .large_file_upload_session import LargeFileUploadSession 2 | from .page_result import PageResult 3 | from .upload_result import UploadResult, UploadSessionDataHolder 4 | 5 | __all__ = ['PageResult', 'LargeFileUploadSession', 'UploadResult', 'UploadSessionDataHolder'] 6 | -------------------------------------------------------------------------------- /src/msgraph_core/models/large_file_upload_session.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | from collections.abc import Callable 5 | from dataclasses import dataclass, field 6 | from typing import Any, Optional 7 | 8 | from kiota_abstractions.serialization import ( 9 | AdditionalDataHolder, 10 | Parsable, 11 | ParseNode, 12 | SerializationWriter, 13 | ) 14 | 15 | 16 | @dataclass 17 | class LargeFileUploadSession(AdditionalDataHolder, Parsable): 18 | 19 | additional_data: dict[str, Any] = field(default_factory=dict) 20 | expiration_date_time: Optional[datetime.datetime] = None 21 | next_expected_ranges: Optional[list[str]] = None 22 | is_cancelled: Optional[bool] = False 23 | odata_type: Optional[str] = None 24 | # The URL endpoint that accepts PUT requests for byte ranges of the file. 25 | upload_url: Optional[str] = None 26 | 27 | @staticmethod 28 | def create_from_discriminator_value( 29 | parse_node: Optional[ParseNode] = None 30 | ) -> LargeFileUploadSession: 31 | """ 32 | Creates a new instance of the appropriate class based 33 | on discriminator value param parse_node: The parse node 34 | to use to read the discriminator value and create the object 35 | Returns: UploadSession 36 | """ 37 | if not parse_node: 38 | raise TypeError("parse_node cannot be null.") 39 | return LargeFileUploadSession() 40 | 41 | def get_field_deserializers(self, ) -> dict[str, Callable[[ParseNode], None]]: 42 | """ 43 | The deserialization information for the current model 44 | Returns: dict[str, Callable[[ParseNode], None]] 45 | """ 46 | fields: dict[str, Callable[[Any], None]] = { 47 | "expirationDateTime": 48 | lambda n: setattr(self, 'expiration_date_time', n.get_datetime_value()), 49 | "nextExpectedRanges": 50 | lambda n: 51 | setattr(self, 'next_expected_ranges', n.get_collection_of_primitive_values(str)), 52 | "@odata.type": 53 | lambda n: setattr(self, 'odata_type', n.get_str_value()), 54 | "uploadUrl": 55 | lambda n: setattr(self, 'upload_url', n.get_str_value()), 56 | } 57 | return fields 58 | 59 | def serialize(self, writer: SerializationWriter) -> None: 60 | """ 61 | Serializes information the current object 62 | param writer: Serialization writer to use to serialize this model 63 | Returns: None 64 | """ 65 | if not writer: 66 | raise TypeError("writer cannot be null.") 67 | writer.write_datetime_value("expirationDateTime", self.expiration_date_time) 68 | writer.write_collection_of_primitive_values("nextExpectedRanges", self.next_expected_ranges) 69 | writer.write_str_value("@odata.type", self.odata_type) 70 | writer.write_str_value("uploadUrl", self.upload_url) 71 | writer.write_additional_data_value(self.additional_data) 72 | -------------------------------------------------------------------------------- /src/msgraph_core/models/page_result.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines the PageResult class which represents a page of 3 | items in a paged response. 4 | 5 | The PageResult class provides methods to get and set the next link and 6 | the items in the page, create a PageResult from a discriminator value, set 7 | the value, get the field deserializers, and serialize the PageResult. 8 | 9 | Classes: 10 | PageResult: Represents a page of items in a paged response. 11 | """ 12 | from __future__ import annotations 13 | 14 | from collections.abc import Callable 15 | from dataclasses import dataclass 16 | from typing import Optional, TypeVar 17 | 18 | from kiota_abstractions.serialization.parsable import Parsable 19 | from kiota_abstractions.serialization.parse_node import ParseNode 20 | from kiota_abstractions.serialization.serialization_writer import SerializationWriter 21 | 22 | T = TypeVar('T') 23 | 24 | 25 | @dataclass 26 | class PageResult(Parsable): 27 | odata_next_link: Optional[str] = None 28 | value: Optional[list[Parsable]] = None 29 | 30 | @staticmethod 31 | def create_from_discriminator_value(parse_node: Optional[ParseNode] = None) -> PageResult: 32 | """ 33 | Creates a new instance of the appropriate class based on discriminator value 34 | Args: 35 | parseNode: The parse node to use to read the discriminator value and create the object 36 | Returns: Attachment 37 | """ 38 | if not parse_node: 39 | raise TypeError("parse_node cannot be null") 40 | return PageResult() 41 | 42 | def get_field_deserializers(self) -> dict[str, Callable[[ParseNode], None]]: 43 | """Gets the deserialization information for this object. 44 | 45 | Returns: 46 | dict[str, Callable[[ParseNode], None]]: The deserialization information for this 47 | object where each entry is a property key with its deserialization callback. 48 | """ 49 | return { 50 | "@odata.nextLink": 51 | lambda x: setattr(self, "odata_next_link", x.get_str_value()), 52 | "value": 53 | lambda x: setattr( 54 | self, 55 | "value", 56 | x.get_collection_of_object_values( 57 | Parsable # type: ignore 58 | # Bug. Should get a collection of primitive dictionary objects 59 | ) 60 | ) 61 | } 62 | 63 | def serialize(self, writer: SerializationWriter) -> None: 64 | """Writes the objects properties to the current writer. 65 | 66 | Args: 67 | writer (SerializationWriter): The writer to write to. 68 | """ 69 | if not writer: 70 | raise TypeError("Writer cannot be null") 71 | writer.write_str_value("@odata.nextLink", self.odata_next_link) 72 | writer.write_collection_of_object_values("value", self.value) 73 | -------------------------------------------------------------------------------- /src/msgraph_core/models/upload_result.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from dataclasses import dataclass 3 | from datetime import datetime 4 | from typing import Any, Generic, Optional, TypeVar 5 | 6 | from kiota_abstractions.serialization import ( 7 | AdditionalDataHolder, 8 | Parsable, 9 | ParseNode, 10 | SerializationWriter, 11 | ) 12 | 13 | T = TypeVar('T') 14 | 15 | 16 | @dataclass 17 | class UploadSessionDataHolder(AdditionalDataHolder, Parsable): 18 | expiration_date_time: Optional[datetime] = None 19 | next_expected_ranges: Optional[list[str]] = None 20 | upload_url: Optional[str] = None 21 | odata_type: Optional[str] = None 22 | 23 | def get_field_deserializers(self, ) -> dict[str, Callable[[ParseNode], None]]: 24 | """ 25 | The deserialization information for the current model 26 | Returns: dict[str, Callable[[ParseNode], None]] 27 | """ 28 | fields: dict[str, Callable[[Any], None]] = { 29 | "expirationDateTime": 30 | lambda n: setattr(self, 'expiration_date_time', n.get_datetime_value()), 31 | "nextExpectedRanges": 32 | lambda n: 33 | setattr(self, 'next_expected_ranges', n.get_collection_of_primitive_values(str)), 34 | "@odata.type": 35 | lambda n: setattr(self, 'odata_type', n.get_str_value()), 36 | "uploadUrl": 37 | lambda n: setattr(self, 'upload_url', n.get_str_value()), 38 | } 39 | return fields 40 | 41 | def serialize(self, writer: SerializationWriter) -> None: 42 | """ 43 | Serializes information the current object 44 | param writer: Serialization writer to use to serialize this model 45 | Returns: None 46 | """ 47 | if not writer: 48 | raise TypeError("writer cannot be null.") 49 | writer.write_datetime_value("expirationDateTime", self.expiration_date_time) 50 | writer.write_collection_of_primitive_values("nextExpectedRanges", self.next_expected_ranges) 51 | writer.write_str_value("@odata.type", self.odata_type) 52 | writer.write_str_value("uploadUrl", self.upload_url) 53 | writer.write_additional_data_value(self.additional_data) 54 | 55 | 56 | class UploadResult(Generic[T]): 57 | 58 | def __init__(self) -> None: 59 | self.upload_session: Optional[UploadSessionDataHolder] = None 60 | self.item_response: Optional[T] = None 61 | self.location: Optional[str] = None 62 | 63 | @property 64 | def upload_succeeded(self) -> bool: 65 | return self.item_response is not None or self.location is not None 66 | -------------------------------------------------------------------------------- /src/msgraph_core/py.typed.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-python-core/849b14ae70ae509f12fe1f983db859f099335580/src/msgraph_core/py.typed.txt -------------------------------------------------------------------------------- /src/msgraph_core/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-python-core/849b14ae70ae509f12fe1f983db859f099335580/src/msgraph_core/requests/__init__.py -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_request_builder.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional, Type, TypeVar, Union 3 | 4 | from kiota_abstractions.api_error import APIError 5 | from kiota_abstractions.headers_collection import HeadersCollection 6 | from kiota_abstractions.method import Method 7 | from kiota_abstractions.request_adapter import RequestAdapter 8 | from kiota_abstractions.request_information import RequestInformation 9 | from kiota_abstractions.serialization import Parsable, ParsableFactory 10 | 11 | from .batch_request_content import BatchRequestContent 12 | from .batch_request_content_collection import BatchRequestContentCollection 13 | from .batch_response_content import BatchResponseContent 14 | from .batch_response_content_collection import BatchResponseContentCollection 15 | 16 | T = TypeVar('T', bound='Parsable') 17 | 18 | APPLICATION_JSON = "application/json" 19 | 20 | 21 | class BatchRequestBuilder: 22 | """ 23 | Provides operations to call the batch method. 24 | """ 25 | 26 | def __init__( 27 | self, 28 | request_adapter: RequestAdapter, 29 | error_map: Optional[dict[str, Type[ParsableFactory]]] = None 30 | ): 31 | if request_adapter is None: 32 | raise ValueError("request_adapter cannot be Null.") 33 | self._request_adapter = request_adapter 34 | self.url_template = f"{self._request_adapter.base_url.removesuffix('/')}/$batch" 35 | self.error_map = error_map or {} 36 | 37 | async def post( 38 | self, 39 | batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], 40 | error_map: Optional[dict[str, Type[ParsableFactory]]] = None, 41 | ) -> Union[BatchResponseContent, BatchResponseContentCollection]: 42 | """ 43 | Sends a batch request and returns the batch response content. 44 | 45 | Args: 46 | batch_request_content (Union[BatchRequestContent, 47 | BatchRequestContentCollection]): The batch request content. 48 | Optional[dict[str, Type[ParsableFactory]]] = None: 49 | Error mappings for response handling. 50 | 51 | Returns: 52 | Union[BatchResponseContent, BatchResponseContentCollection]: The batch response content 53 | or the specified response type. 54 | 55 | """ 56 | if batch_request_content is None: 57 | raise ValueError("batch_request_content cannot be Null.") 58 | response_type = BatchResponseContent 59 | 60 | if isinstance(batch_request_content, BatchRequestContent): 61 | request_info = await self.to_post_request_information(batch_request_content) 62 | error_map = error_map or self.error_map 63 | response = None 64 | try: 65 | response = await self._request_adapter.send_async( 66 | request_info, response_type, error_map 67 | ) 68 | 69 | except APIError as e: 70 | logging.error("API Error: %s", e) 71 | raise e 72 | if response is None: 73 | raise ValueError("Failed to get a valid response from the API.") 74 | return response 75 | if isinstance(batch_request_content, BatchRequestContentCollection): 76 | batch_responses = await self._post_batch_collection(batch_request_content, error_map) 77 | return batch_responses 78 | 79 | raise ValueError("Invalid type for batch_request_content.") 80 | 81 | async def _post_batch_collection( 82 | self, 83 | batch_request_content_collection: BatchRequestContentCollection, 84 | error_map: Optional[dict[str, Type[ParsableFactory]]] = None, 85 | ) -> BatchResponseContentCollection: 86 | """ 87 | Sends a collection of batch requests and returns a collection of batch response contents. 88 | 89 | Args: 90 | batch_request_content_collection (BatchRequestContentCollection): The 91 | collection of batch request contents. 92 | Optional[dict[str, Type[ParsableFactory]]] = None: 93 | Error mappings for response handling. 94 | 95 | Returns: 96 | BatchResponseContentCollection: The collection of batch response contents. 97 | """ 98 | if batch_request_content_collection is None: 99 | raise ValueError("batch_request_content_collection cannot be Null.") 100 | 101 | batch_responses = BatchResponseContentCollection() 102 | batch_requests = batch_request_content_collection.get_batch_requests_for_execution() 103 | for batch_request_content in batch_requests: 104 | response = await self.post(batch_request_content, error_map) 105 | if isinstance(response, BatchResponseContent): 106 | batch_responses.add_response(response) 107 | 108 | return batch_responses 109 | 110 | async def to_post_request_information( 111 | self, batch_request_content: BatchRequestContent 112 | ) -> RequestInformation: 113 | """ 114 | Creates request information for a batch POST request. 115 | 116 | Args: 117 | batch_request_content (BatchRequestContent): The batch request content. 118 | 119 | Returns: 120 | RequestInformation: The request information. 121 | """ 122 | 123 | if batch_request_content is None: 124 | raise ValueError("batch_request_content cannot be Null.") 125 | 126 | request_info = RequestInformation() 127 | request_info.http_method = Method.POST 128 | request_info.url_template = self.url_template 129 | request_info.headers = HeadersCollection() 130 | request_info.headers.try_add("Content-Type", APPLICATION_JSON) 131 | request_info.set_content_from_parsable( 132 | self._request_adapter, APPLICATION_JSON, batch_request_content 133 | ) 134 | 135 | return request_info 136 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_request_content.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional, Union 3 | from urllib.request import Request 4 | 5 | from kiota_abstractions.request_information import RequestInformation 6 | from kiota_abstractions.serialization import Parsable, ParseNode, SerializationWriter 7 | 8 | from .batch_request_item import BatchRequestItem 9 | 10 | 11 | class BatchRequestContent(Parsable): 12 | """ 13 | Provides operations to call the batch method. 14 | """ 15 | 16 | MAX_REQUESTS = 20 17 | 18 | def __init__(self, requests: dict[str, Union[BatchRequestItem, RequestInformation]] = {}): 19 | """ 20 | Initializes a new instance of the BatchRequestContent class. 21 | Args: 22 | Requests (dict[str, Union[BatchRequestItem, RequestInformation]]): The requests to add. 23 | """ 24 | self._requests: dict[str, BatchRequestItem] = {} 25 | 26 | self.is_finalized = False 27 | for request_id, request in requests.items(): 28 | if isinstance(request, RequestInformation): 29 | self.add_request_information(request, request_id) 30 | continue 31 | self.add_request(request_id, request) 32 | 33 | @property 34 | def requests(self) -> dict[str, BatchRequestItem]: 35 | """ 36 | Gets the requests. 37 | Returns: 38 | dict[str, BatchRequestItem]: requests in the batch request content. 39 | """ 40 | return self._requests 41 | 42 | @requests.setter 43 | def requests(self, requests: list[BatchRequestItem]) -> None: 44 | """ 45 | Sets the requests. 46 | Args: 47 | requests (list[BatchRequestItem]): The requests to set. 48 | """ 49 | if len(requests) >= BatchRequestContent.MAX_REQUESTS: 50 | raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") 51 | for request in requests: 52 | self.add_request(request.id, request) 53 | 54 | def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> None: 55 | """ 56 | Adds a request to the batch request content. 57 | Args: 58 | request_id (Optional[str]): The request id to add. 59 | request (BatchRequestItem): The request to add. 60 | """ 61 | if len(self.requests) >= BatchRequestContent.MAX_REQUESTS: 62 | raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") 63 | if not request.id: 64 | request.id = request_id if request_id else str(uuid.uuid4()) 65 | if hasattr(request, 'depends_on') and request.depends_on: 66 | for dependent_id in request.depends_on: 67 | if not self._request_by_id(dependent_id): 68 | raise ValueError( 69 | f""" 70 | Request depends on request id: {dependent_id} 71 | which was not found in requests. Add request id: {dependent_id} first""" 72 | ) 73 | self._requests[request.id] = request 74 | 75 | def add_request_information( 76 | self, request_information: RequestInformation, request_id: Optional[str] = None 77 | ) -> None: 78 | """ 79 | Adds a request to the batch request content. 80 | Args: 81 | request_information (RequestInformation): The request information to add. 82 | request_id: Optional[str]: The request id to add. 83 | """ 84 | request_id = request_id if request_id else str(uuid.uuid4()) 85 | self.add_request(request_id, BatchRequestItem(request_information)) 86 | 87 | def add_urllib_request(self, request: Request, request_id: Optional[str] = None) -> None: 88 | """ 89 | Adds a request to the batch request content. 90 | Args: 91 | request (Request): The request to add. 92 | request_id: Optional[str]: The request id to add. 93 | """ 94 | request_id = request_id if request_id else str(uuid.uuid4()) 95 | self.add_request(request_id, BatchRequestItem.create_with_urllib_request(request)) 96 | 97 | def remove(self, request_id: str) -> None: 98 | """ 99 | Removes a request from the batch request content. 100 | Also removes the request from the depends_on list of 101 | other requests. 102 | Args: 103 | request_id (str): The request id to remove. 104 | """ 105 | request_to_remove = None 106 | for request in self.requests.values(): 107 | if request.id == request_id: 108 | request_to_remove = request 109 | if hasattr(request, 'depends_on') and request.depends_on: 110 | if request_id in request.depends_on: 111 | request.depends_on.remove(request_id) 112 | if request_to_remove: 113 | del self._requests[request_to_remove.id] 114 | else: 115 | raise ValueError(f"Request ID {request_id} not found in requests.") 116 | 117 | def remove_batch_request_item(self, item: BatchRequestItem) -> None: 118 | """ 119 | Removes a request from the batch request content. 120 | """ 121 | self.remove(item.id) 122 | 123 | def finalize(self): 124 | """ 125 | Finalizes the batch request content. 126 | """ 127 | self.is_finalized = True 128 | return self 129 | 130 | def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: 131 | """ 132 | Finds a request by its ID. 133 | 134 | Args: 135 | request_id (str): The ID of the request to find. 136 | 137 | Returns: 138 | Optional[BatchRequestItem]: The request with the given ID, or None if not found. 139 | """ 140 | return self._requests.get(request_id) 141 | 142 | @staticmethod 143 | def create_from_discriminator_value( 144 | parse_node: Optional[ParseNode] = None 145 | ) -> 'BatchRequestContent': 146 | if parse_node is None: 147 | raise ValueError("parse_node cannot be None") 148 | return BatchRequestContent() 149 | 150 | def get_field_deserializers(self, ) -> dict: 151 | """ 152 | The deserialization information for the current model 153 | """ 154 | return {} 155 | 156 | def serialize(self, writer: SerializationWriter) -> None: 157 | """ 158 | Serializes information the current object 159 | Args: 160 | writer: Serialization writer to use to serialize this model 161 | """ 162 | writer.write_collection_of_object_values("requests", list(self.requests.values())) 163 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_request_content_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from kiota_abstractions.serialization import SerializationWriter 4 | 5 | from .batch_request_content import BatchRequestContent 6 | from .batch_request_item import BatchRequestItem 7 | 8 | 9 | class BatchRequestContentCollection: 10 | """A collection of request content objects.""" 11 | 12 | def __init__(self) -> None: 13 | """ 14 | Initializes a new instance of the BatchRequestContentCollection class. 15 | 16 | 17 | """ 18 | self.max_requests_per_batch = BatchRequestContent.MAX_REQUESTS 19 | self.current_batch: BatchRequestContent = BatchRequestContent() 20 | self.batches: list[BatchRequestContent] = [self.current_batch] 21 | 22 | def add_batch_request_item(self, request: BatchRequestItem) -> None: 23 | """ 24 | Adds a request item to the collection. 25 | Args: 26 | request (BatchRequestItem): The request item to add. 27 | """ 28 | if len(self.current_batch.requests) >= self.max_requests_per_batch: 29 | self.current_batch.finalize() 30 | self.current_batch = BatchRequestContent() 31 | self.batches.append(self.current_batch) 32 | self.current_batch.add_request(request.id, request) 33 | 34 | def remove_batch_request_item(self, request_id: str) -> None: 35 | """ 36 | Removes a request item from the collection. 37 | Args: 38 | request_id (str): The ID of the request item to remove. 39 | """ 40 | for batch in self.batches: 41 | if request_id in batch.requests: 42 | batch.remove(request_id) 43 | return 44 | 45 | def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: 46 | """ 47 | Creates a new batch with failed requests. 48 | Returns: 49 | Optional[BatchRequestContent]: A new batch with failed requests. 50 | """ 51 | # Use IDs to get response status codes, generate new batch with failed requests 52 | batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent() 53 | for batch in self.batches: 54 | for request in batch.requests: 55 | if request.status_code not in [ # type: ignore # Method should be deprecated 56 | 200, 201, 202, 203, 204, 205, 206, 207, 208, 226 57 | ]: 58 | if batch_with_failed_responses is not None: 59 | batch_with_failed_responses.add_request( 60 | request.id, # type: ignore # Bug. Method should be deprecated 61 | request # type: ignore 62 | ) 63 | else: 64 | raise ValueError("batch_with_failed_responses is None") 65 | return batch_with_failed_responses 66 | 67 | def get_batch_requests_for_execution(self) -> list[BatchRequestContent]: 68 | """ 69 | Gets the batch requests for execution. 70 | Returns: 71 | list[BatchRequestContent]: The batch requests for execution. 72 | """ 73 | return self.batches 74 | 75 | def serialize(self, writer: SerializationWriter) -> None: 76 | """ 77 | Serializes information the current object 78 | Args: 79 | writer: Serialization writer to use to serialize this model 80 | """ 81 | pass 82 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_request_item.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import enum 3 | import json 4 | import re 5 | import urllib.request 6 | from deprecated import deprecated 7 | from io import BytesIO 8 | from typing import Any, Optional, Union 9 | from urllib.parse import urlparse 10 | from uuid import uuid4 11 | 12 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 13 | from kiota_abstractions.method import Method 14 | from kiota_abstractions.request_information import RequestInformation 15 | from kiota_abstractions.serialization import Parsable, ParseNode, SerializationWriter 16 | 17 | 18 | @deprecated("Use BytesIO type instead") 19 | class StreamInterface(BytesIO): 20 | pass 21 | 22 | 23 | class BatchRequestItem(Parsable): 24 | API_VERSION_REGEX = re.compile(r'/\/(v1.0|beta)/') 25 | ME_TOKEN_REGEX = re.compile(r'/\/users\/me-token-to-replace/') 26 | 27 | def __init__( 28 | self, 29 | request_information: Optional[RequestInformation] = None, 30 | id: str = "", 31 | depends_on: Optional[list[Union[str, 'BatchRequestItem']]] = [] 32 | ): 33 | """ 34 | Initializes a new instance of the BatchRequestItem class. 35 | Args: 36 | request_information (RequestInformation): The request information. 37 | id (str, optional): The ID of the request item. Defaults to "". 38 | depends_on (Optional[list[Union[str, BatchRequestItem]], optional): 39 | The IDs of the requests that this request depends on. Defaults to None. 40 | """ 41 | if request_information is None or not request_information.http_method: 42 | raise ValueError("HTTP method cannot be Null/Empty") 43 | self._id = id or str(uuid4()) 44 | if isinstance(request_information.http_method, enum.Enum): 45 | self._method = request_information.http_method.name 46 | else: 47 | self._method = request_information.http_method 48 | self._headers: Optional[dict[str, str]] = request_information.request_headers 49 | self._body = request_information.content 50 | self.url = request_information.url.replace('/users/me-token-to-replace', '/me', 1) 51 | self._depends_on: Optional[list[str]] = [] 52 | if depends_on is not None: 53 | self.set_depends_on(depends_on) 54 | 55 | @staticmethod 56 | def create_with_urllib_request( 57 | request: urllib.request.Request, 58 | id: str = "", 59 | depends_on: Optional[list[str]] = None 60 | ) -> 'BatchRequestItem': 61 | """ 62 | Creates a new instance of the BatchRequestItem class from a urllib request. 63 | Args: 64 | request (urllib.request.Request): The urllib request. 65 | id (str, optional): The ID of the request item. Defaults to "". 66 | depends_on (Optional[list[str]], optional): The IDs of 67 | the requests that this request depends on. Defaults to None. 68 | Returns: 69 | BatchRequestItem: A new instance of the BatchRequestItem class. 70 | """ 71 | request_info = RequestInformation() 72 | try: 73 | request_info.http_method = Method[request.get_method().upper()] 74 | except KeyError: 75 | raise KeyError(f"Request Method: {request.get_method()} is invalid") 76 | 77 | request_info.url = request.full_url 78 | request_info.headers = RequestHeaders() 79 | for key, value in request.headers.items(): 80 | request_info.headers.try_add(header_name=key, header_value=value) 81 | request_info.content = request.data # type: ignore 82 | return BatchRequestItem( 83 | request_info, 84 | id, 85 | depends_on # type: ignore # union types not analysed correctly 86 | ) 87 | 88 | def set_depends_on(self, requests: Optional[list[Union[str, 'BatchRequestItem']]]) -> None: 89 | """ 90 | Sets the IDs of the requests that this request depends on. 91 | Args: 92 | requests (Optional[list[Union[str, BatchRequestItem]]): The 93 | IDs of the requests that this request depends on. 94 | """ 95 | if requests: 96 | for request in requests: 97 | if self._depends_on is None: 98 | self._depends_on = [] 99 | self._depends_on.append(request if isinstance(request, str) else request.id) 100 | 101 | def set_url(self, url: str) -> None: 102 | """ 103 | Sets the URL of the request. 104 | Args: 105 | url (str): The URL of the request. 106 | """ 107 | url_parts = urlparse(url) 108 | if not url_parts.path: 109 | raise ValueError(f"Invalid URL {url}") 110 | 111 | relative_url = re.sub(BatchRequestItem.API_VERSION_REGEX, '', url_parts.path, 1) 112 | if not relative_url: 113 | raise ValueError( 114 | f"Error occurred during regex replacement of API version in URL string: {url}" 115 | ) 116 | 117 | relative_url = relative_url.replace('/users/me-token-to-replace', '/me', 1) 118 | if not relative_url: 119 | raise ValueError( 120 | f"""Error occurred during regex replacement 121 | of '/users/me-token-to-replace' in URL string: {url}""" 122 | ) 123 | self.url = relative_url 124 | if url_parts.query: 125 | self.url += f"?{url_parts.query}" 126 | if url_parts.fragment: 127 | self.url += f"#{url_parts.fragment}" 128 | 129 | @property 130 | def id(self) -> str: 131 | """ 132 | Gets the ID of the request item. 133 | Returns: 134 | str: The ID of the request item. 135 | """ 136 | return self._id 137 | 138 | @id.setter 139 | def id(self, value: str) -> None: 140 | """ 141 | Sets the ID of the request item. 142 | Args: 143 | value (str): The ID of the request item. 144 | """ 145 | self._id = value 146 | 147 | @property 148 | def headers(self) -> Optional[dict[str, str]]: 149 | """ 150 | Gets the headers of the request item. 151 | Returns: 152 | Optional[dict[str, str]]: The headers of the request item. 153 | """ 154 | return self._headers 155 | 156 | @headers.setter 157 | def headers(self, headers: dict[str, Union[list[str], str]]) -> None: 158 | """ 159 | Sets the headers of the request item. 160 | Args: 161 | headers (dict[str, Union[list[str], str]]): The headers of the request item. 162 | """ 163 | if self._headers: 164 | self._headers.clear() 165 | else: 166 | self._headers = {} 167 | headers_collection = RequestHeaders() 168 | for header, value in headers.items(): 169 | headers_collection.add(header, value) 170 | for key, values in headers_collection.get_all().items(): 171 | self._headers[key] = ', '.join(values) 172 | 173 | @property 174 | def body(self) -> Optional[bytes]: 175 | """ 176 | Gets the body of the request item. 177 | Returns: 178 | Optional[bytes]: The body of the request item. 179 | """ 180 | return self._body 181 | 182 | @body.setter 183 | def body(self, body: BytesIO) -> None: 184 | """ 185 | Sets the body of the request item. 186 | Args: 187 | body : (BytesIO): The body of the request item. 188 | """ 189 | self._body = body.getvalue() 190 | 191 | @property 192 | def method(self) -> str: 193 | """ 194 | Gets the HTTP method of the request item. 195 | Returns: 196 | str: The HTTP method of the request item. 197 | """ 198 | return self._method 199 | 200 | @method.setter 201 | def method(self, value: str) -> None: 202 | """ 203 | Sets the HTTP method of the request item. 204 | Args: 205 | value (str): The HTTP method of the request item. 206 | 207 | """ 208 | 209 | self._method = value 210 | 211 | @property 212 | def depends_on(self) -> Optional[list[str]]: 213 | """ 214 | Gets the IDs of the requests that this request depends on. 215 | Returns: 216 | Optional[list[str]]: The IDs of the requests that this request depends on. 217 | """ 218 | return self._depends_on 219 | 220 | @staticmethod 221 | def create_from_discriminator_value( 222 | parse_node: Optional[ParseNode] = None 223 | ) -> 'BatchRequestItem': 224 | """ 225 | Creates a new instance of the appropriate class based 226 | on discriminator value param parse_node: The parse node 227 | to use to read the discriminator value and create the object 228 | Returns: BatchRequestItem 229 | """ 230 | if not parse_node: 231 | raise TypeError("parse_node cannot be null.") 232 | return BatchRequestItem() 233 | 234 | def get_field_deserializers(self) -> dict[str, Any]: 235 | """ 236 | Gets the deserialization information for this object. 237 | Returns: 238 | dict[str, Any]: The deserialization information for 239 | this object where each entry is a property key with its 240 | deserialization callback. 241 | """ 242 | return { 243 | "id": self._id, 244 | "method": self.method, 245 | "url": self.url, 246 | "headers": self._headers, 247 | "body": self._body, 248 | "depends_on": self._depends_on 249 | } 250 | 251 | def serialize(self, writer: SerializationWriter) -> None: 252 | """ 253 | Writes the objects properties to the current writer. 254 | Args: 255 | writer (SerializationWriter): The writer to write to. 256 | """ 257 | writer.write_str_value('id', self.id) 258 | writer.write_str_value('method', self.method) 259 | writer.write_str_value('url', self.url) 260 | writer.write_collection_of_primitive_values('depends_on', self._depends_on) 261 | writer.write_collection_of_object_values( 262 | 'headers', 263 | self._headers # type: ignore # need method to serialize dicts 264 | ) 265 | if self._body: 266 | json_object = json.loads(self._body) 267 | is_json_string = json_object and isinstance(json_object, dict) 268 | # /$batch API expects JSON object or base 64 encoded value for the body 269 | if is_json_string: 270 | writer.write_collection_of_object_values( # type: ignore 271 | # need method to serialize dicts 272 | 'body', 273 | json_object 274 | ) 275 | else: 276 | writer.write_str_value('body', base64.b64encode(self._body).decode('utf-8')) 277 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_response_content.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from collections.abc import Callable 3 | from io import BytesIO 4 | from typing import Optional, Type, TypeVar, Union 5 | 6 | from kiota_abstractions.serialization import ( 7 | Parsable, 8 | ParsableFactory, 9 | ParseNode, 10 | ParseNodeFactoryRegistry, 11 | SerializationWriter, 12 | ) 13 | 14 | from .batch_response_item import BatchResponseItem 15 | 16 | T = TypeVar('T', bound=ParsableFactory) 17 | 18 | 19 | class BatchResponseContent(Parsable): 20 | 21 | def __init__(self) -> None: 22 | """ 23 | Initializes a new instance of the BatchResponseContent class. 24 | BatchResponseContent is a collection of BatchResponseItem items, 25 | each with a unique request ID. 26 | """ 27 | self._responses: Optional[dict[str, BatchResponseItem]] = {} 28 | 29 | @property 30 | def responses(self) -> Optional[dict[str, BatchResponseItem]]: 31 | """ 32 | Get the responses in the collection 33 | :return: A dictionary of response IDs and their BatchResponseItem objects 34 | :rtype: Optional[dict[str, BatchResponseItem]] 35 | """ 36 | return self._responses 37 | 38 | @responses.setter 39 | def responses(self, responses: Optional[dict[str, BatchResponseItem]]) -> None: 40 | """ 41 | Set the responses in the collection 42 | :param responses: The responses to set in the collection 43 | :type responses: Optional[dict[str, BatchResponseItem]] 44 | """ 45 | self._responses = responses 46 | 47 | def get_response_by_id( 48 | self, 49 | request_id: str, 50 | response_type: Optional[Type[T]] = None, 51 | ) -> Optional[Union[T, BatchResponseItem]]: 52 | """ 53 | Get a response by its request ID from the collection 54 | :param request_id: The request ID of the response to get 55 | :type request_id: str 56 | :return: The response with the specified request ID as a BatchResponseItem 57 | :rtype: BatchResponseItem 58 | """ 59 | if self._responses is None: 60 | return None 61 | if response_type is not None: 62 | return self.response_body(request_id, response_type) 63 | return self._responses.get(request_id) 64 | 65 | def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: 66 | """ 67 | Get a response by its request ID and return the body as a stream 68 | :param request_id: The request ID of the response to get 69 | :type request_id: str 70 | :return: The response Body as a stream 71 | :rtype: BytesIO 72 | """ 73 | response_item = self.get_response_by_id(request_id) 74 | if response_item is None or response_item.body is None: 75 | return None 76 | 77 | if isinstance(response_item.body, BytesIO): 78 | return response_item.body 79 | return BytesIO(response_item.body) 80 | 81 | def get_response_status_codes(self) -> dict[str, int]: 82 | """ 83 | Go through responses and for each, append {'request-id': status_code} to a dictionary. 84 | :return: A dictionary with request_id as keys and status_code as values. 85 | :rtype: dict 86 | """ 87 | status_codes: dict[str, int] = {} 88 | if self._responses is None: 89 | return status_codes 90 | 91 | for request_id, response_item in self._responses.items(): 92 | if response_item is not None and response_item.status is not None: 93 | status_codes[request_id] = response_item.status 94 | 95 | return status_codes 96 | 97 | def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: 98 | """ 99 | Get the body of a response by its request ID from the collection 100 | :param request_id: The request ID of the response to get 101 | :type request_id: str 102 | :param type: The type to deserialize the response body to 103 | :type type: Type[T] 104 | :return: The deserialized response body 105 | :rtype: Optional[T] 106 | """ 107 | if not self._responses or request_id not in self._responses: 108 | raise ValueError(f"No response found for id: {request_id}") 109 | 110 | if not issubclass(type, Parsable): 111 | raise ValueError("Type passed must implement the Parsable interface") 112 | 113 | response = self.get_response_by_id(request_id) 114 | if response is not None: 115 | content_type = response.content_type 116 | else: 117 | raise ValueError( 118 | f"Unable to get content-type header in response item for request Id: {request_id}" 119 | ) 120 | if not content_type: 121 | raise RuntimeError("Unable to get content-type header in response item") 122 | 123 | response_body = response.body or BytesIO() 124 | try: 125 | try: 126 | parse_node = ParseNodeFactoryRegistry().get_root_parse_node( 127 | content_type, response_body 128 | ) 129 | except Exception: 130 | response_body.seek(0) 131 | base64_decoded_body = BytesIO(base64.b64decode(response_body.read())) 132 | parse_node = ParseNodeFactoryRegistry().get_root_parse_node( 133 | content_type, base64_decoded_body 134 | ) 135 | response.body = base64_decoded_body 136 | return parse_node.get_object_value(type) 137 | except Exception: 138 | raise ValueError( 139 | f"Unable to deserialize batch response for request Id: {request_id} to {type}" 140 | ) 141 | 142 | def get_field_deserializers(self) -> dict[str, Callable[[ParseNode], None]]: 143 | """ 144 | Gets the deserialization information for this object. 145 | :return: The deserialization information for this object 146 | :rtype: dict[str, Callable[[ParseNode], None]] 147 | """ 148 | 149 | def set_responses(n: ParseNode): 150 | values = n.get_collection_of_object_values(BatchResponseItem) 151 | if values: 152 | setattr(self, '_responses', {item.id: item for item in values}) 153 | else: 154 | setattr(self, '_responses', {}) 155 | 156 | return {'responses': lambda n: set_responses(n)} 157 | 158 | def serialize(self, writer: SerializationWriter) -> None: 159 | """ 160 | Writes the objects properties to the current writer. 161 | :param writer: The writer to write to 162 | """ 163 | if self._responses is not None: 164 | writer.write_collection_of_object_values('responses', list(self._responses.values())) 165 | else: 166 | writer.write_collection_of_object_values('responses', []) 167 | 168 | @staticmethod 169 | def create_from_discriminator_value( 170 | parse_node: Optional[ParseNode] = None 171 | ) -> 'BatchResponseContent': 172 | if parse_node is None: 173 | raise ValueError("parse_node cannot be None") 174 | return BatchResponseContent() 175 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_response_content_collection.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | 3 | from kiota_abstractions.serialization import Parsable, ParseNode, SerializationWriter 4 | 5 | from .batch_response_content import BatchResponseContent 6 | from .batch_response_item import BatchResponseItem 7 | 8 | 9 | class BatchResponseContentCollection(Parsable): 10 | 11 | def __init__(self) -> None: 12 | """ 13 | Initializes a new instance of the BatchResponseContentCollection class. 14 | BatchResponseContentCollection is a collection of BatchResponseContent items, each with 15 | a unique request ID. 16 | headers: Optional[dict[str, str]] = {} 17 | status_code: Optional[int] = None 18 | body: Optional[StreamInterface] = None 19 | 20 | """ 21 | self._responses: list[BatchResponseContent] = [] 22 | 23 | def add_response(self, response: BatchResponseContent) -> None: 24 | """ 25 | Adds a response to the collection. 26 | Args: 27 | keys: The keys of the response to add. 28 | response: The response to add. 29 | """ 30 | self._responses.append(response) 31 | 32 | def get_responses(self): 33 | """ 34 | Gets the responses in the collection. 35 | Returns: 36 | list[Tuple[str, BatchResponseContent]]: The responses in the collection. 37 | """ 38 | return self._responses 39 | 40 | @property 41 | async def responses_status_codes(self) -> dict[str, int]: 42 | """ 43 | Get the status codes of all responses in the collection 44 | :return: A dictionary of response IDs and their status codes 45 | :rtype: dict[str, int] 46 | """ 47 | status_codes: dict[str, int] = {} 48 | for response in self._responses: 49 | if isinstance(response, BatchResponseItem): 50 | if response.id is not None: 51 | status_codes[response.id] = response.status 52 | else: 53 | raise ValueError("Response ID cannot be None") 54 | else: 55 | raise TypeError("Invalid type: Collection must be of type BatchResponseContent") 56 | return status_codes 57 | 58 | def get_field_deserializers(self) -> dict[str, Callable[[ParseNode], None]]: 59 | """ 60 | Gets the deserialization information for this object. 61 | :return: The deserialization information for this object where each entry is a property key 62 | with its deserialization callback. 63 | :rtype: dict[str, Callable[[ParseNode], None]] 64 | """ 65 | return { 66 | 'responses': 67 | lambda n: 68 | setattr(self, "_responses", n.get_collection_of_object_values(BatchResponseItem)) 69 | } 70 | 71 | def serialize(self, writer: SerializationWriter) -> None: 72 | """ 73 | Writes the objects properties to the current writer. 74 | :param writer: The writer to write to. 75 | :type writer: SerializationWriter 76 | """ 77 | writer.write_collection_of_object_values('responses', self._responses) 78 | -------------------------------------------------------------------------------- /src/msgraph_core/requests/batch_response_item.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Callable, Optional 3 | 4 | from deprecated import deprecated 5 | from kiota_abstractions.serialization import ( 6 | Parsable, 7 | ParsableFactory, 8 | ParseNode, 9 | SerializationWriter, 10 | ) 11 | 12 | 13 | @deprecated("Use BytesIO type instead") 14 | class StreamInterface(BytesIO): 15 | pass 16 | 17 | 18 | class BatchResponseItem(Parsable): 19 | 20 | def __init__(self) -> None: 21 | """ 22 | Initializes a new instance of the BatchResponseItem class. 23 | """ 24 | self._id: Optional[str] = None 25 | self._atomicity_group: Optional[str] = None 26 | self._status: Optional[int] = None 27 | self._headers: Optional[dict[str, str]] = {} 28 | self._body: Optional[BytesIO] = None 29 | 30 | @property 31 | def id(self) -> Optional[str]: 32 | """ 33 | Get the ID of the response 34 | :return: The ID of the response 35 | :rtype: Optional[str] 36 | """ 37 | return self._id 38 | 39 | @id.setter 40 | def id(self, id: Optional[str]) -> None: 41 | """ 42 | Set the ID of the response 43 | :param id: The ID of the response 44 | :type id: Optional[str] 45 | """ 46 | self._id = id 47 | 48 | @property 49 | def atomicity_group(self) -> Optional[str]: 50 | """ 51 | Get the atomicity group of the response 52 | :return: The atomicity group of the response 53 | :rtype: Optional[str] 54 | """ 55 | return self._atomicity_group 56 | 57 | @atomicity_group.setter 58 | def atomicity_group(self, atomicity_group: Optional[str]) -> None: 59 | """ 60 | Set the atomicity group of the response 61 | :param atomicity_group: The atomicity group of the response 62 | :type atomicity_group: Optional[str] 63 | """ 64 | self._atomicity_group = atomicity_group 65 | 66 | @property 67 | def status(self) -> Optional[int]: 68 | """ 69 | Get the status code of the response 70 | :return: The status code of the response 71 | :rtype: Optional[int] 72 | """ 73 | return self._status 74 | 75 | @status.setter 76 | def status(self, status_code: Optional[int]) -> None: 77 | """ 78 | Set the status code of the response 79 | :param status_code: The status code of the response 80 | :type status_code: Optional[int] 81 | """ 82 | self._status = status_code 83 | 84 | @property 85 | def headers(self) -> Optional[dict[str, str]]: 86 | """ 87 | Get the headers of the response 88 | :return: The headers of the response 89 | :rtype: Optional[dict[str, str]] 90 | """ 91 | return self._headers 92 | 93 | @headers.setter 94 | def headers(self, headers: Optional[dict[str, str]]) -> None: 95 | """ 96 | Set the headers of the response 97 | :param headers: The headers of the response 98 | :type headers: Optional[dict[str, str]] 99 | """ 100 | self._headers = headers 101 | 102 | @property 103 | def body(self) -> Optional[BytesIO]: 104 | """ 105 | Get the body of the response 106 | :return: The body of the response 107 | :rtype: Optional[BytesIO] 108 | """ 109 | return self._body 110 | 111 | @body.setter 112 | def body(self, body: Optional[BytesIO]) -> None: 113 | """ 114 | Set the body of the response 115 | :param body: The body of the response 116 | :type body: Optional[BytesIO] 117 | """ 118 | self._body = body 119 | 120 | @property 121 | def content_type(self) -> Optional[str]: 122 | """ 123 | Get the content type of the response 124 | :return: The content type of the response 125 | :rtype: Optional[str] 126 | """ 127 | if self.headers: 128 | headers = {k.lower(): v for k, v in self.headers.items()} 129 | return headers.get('content-type') 130 | return None 131 | 132 | @staticmethod 133 | def create_from_discriminator_value( 134 | parse_node: Optional[ParseNode] = None 135 | ) -> 'BatchResponseItem': 136 | """ 137 | Creates a new instance of the appropriate class based on discriminator value 138 | Args: 139 | parse_node: The parse node to use to read the discriminator value and create the object 140 | Returns: BatchResponseItem 141 | """ 142 | if not parse_node: 143 | raise TypeError("parse_node cannot be null") 144 | return BatchResponseItem() 145 | 146 | def get_field_deserializers(self) -> dict[str, Callable[[ParseNode], None]]: 147 | """ 148 | Gets the deserialization information for this object. 149 | 150 | """ 151 | return { 152 | "id": lambda x: setattr(self, "id", x.get_str_value()), 153 | "status": lambda x: setattr(self, "status", x.get_int_value()), 154 | "headers": lambda x: setattr( 155 | self, 156 | "headers", 157 | x.try_get_anything(x._json_node) # type: ignore 158 | ), # need interface to return a dictionary 159 | "body": lambda x: setattr(self, "body", x.get_bytes_value()), 160 | } 161 | 162 | def serialize(self, writer: SerializationWriter) -> None: 163 | """ 164 | Writes the objects properties to the current writer. 165 | """ 166 | writer.write_str_value('id', self._id) 167 | writer.write_str_value('atomicity_group', self._atomicity_group) 168 | writer.write_int_value('status', self._status) 169 | writer.write_collection_of_primitive_values( 170 | 'headers', 171 | self._headers # type: ignore 172 | ) # need method to serialize dicts 173 | if self._body: 174 | writer.write_bytes_value('body', self._body.getvalue()) 175 | else: 176 | writer.write_bytes_value('body', None) 177 | -------------------------------------------------------------------------------- /src/msgraph_core/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .large_file_upload import LargeFileUploadTask 2 | from .page_iterator import PageIterator 3 | 4 | __all__ = ['PageIterator', 'LargeFileUploadTask'] 5 | -------------------------------------------------------------------------------- /src/msgraph_core/tasks/page_iterator.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the PageIterator class which is used to 3 | iterate over paged responses from a server. 4 | 5 | The PageIterator class provides methods to iterate over the items 6 | in the pages, fetch the next page, convert a response to a page, and 7 | fetch the next page from the server. 8 | 9 | The PageIterator class uses the Parsable interface to parse the responses 10 | from the server, the RequestAdapter class to send requests to the 11 | server, and the PageResult class to represent the pages. 12 | 13 | This module also imports the necessary types and exceptions from the 14 | typing, requests.exceptions, kiota_http.httpx_request_adapter, 15 | kiota_abstractions.method, kiota_abstractions.headers_collection, 16 | kiota_abstractions.request_information, kiota_abstractions.serialization.parsable, 17 | and models modules. 18 | """ 19 | 20 | from collections.abc import Callable 21 | from typing import Optional, Type, TypeVar, Union 22 | 23 | from kiota_abstractions.headers_collection import HeadersCollection 24 | from kiota_abstractions.method import Method 25 | from kiota_abstractions.request_adapter import RequestAdapter 26 | from kiota_abstractions.request_information import RequestInformation 27 | from kiota_abstractions.serialization import Parsable, ParsableFactory 28 | from requests.exceptions import InvalidURL 29 | 30 | from msgraph_core.models.page_result import ( 31 | PageResult, # pylint: disable=no-name-in-module, import-error 32 | ) 33 | 34 | T = TypeVar('T', bound=Parsable) 35 | 36 | 37 | class PageIterator: 38 | """ 39 | This class is used to iterate over paged responses from a server. 40 | 41 | The PageIterator class provides methods to iterate over the items in the pages, 42 | fetch the next page, and convert a response to a page. 43 | 44 | Attributes: 45 | request_adapter (RequestAdapter): The adapter used to send HTTP requests. 46 | pause_index (int): The index at which to pause iteration. 47 | headers (HeadersCollection): The headers to include in the HTTP requests. 48 | request_options (list): The options for the HTTP requests. 49 | current_page (PageResult): The current page of items. 50 | object_type (str): The type of the items in the pages. 51 | has_next (bool): Whether there are more pages to fetch. 52 | 53 | Methods: 54 | __init__(response: Union[T, list, object], request_adapter: RequestAdapter, 55 | constructor_callable: Optional[Callable] = None): Initializes a new instance of 56 | the PageIterator class. 57 | """ 58 | 59 | def __init__( 60 | self, 61 | response: Union[T, list, object], 62 | request_adapter: RequestAdapter, 63 | constructor_callable: Optional[Callable] = None, 64 | error_mapping: Optional[dict[str, Type[ParsableFactory]]] = None, 65 | ): 66 | self.request_adapter = request_adapter 67 | 68 | if isinstance(response, Parsable) and not constructor_callable: 69 | parsable_factory: Type[Parsable] = type(response) 70 | elif constructor_callable is None: 71 | parsable_factory = PageResult 72 | else: 73 | raise ValueError( 74 | 'One of the constructor_callable or the PageResult type parameter is required.' 75 | ) 76 | self.parsable_factory = parsable_factory 77 | self.pause_index = 0 78 | self.headers: HeadersCollection = HeadersCollection() 79 | self.request_options: list = [] 80 | self.current_page = self.convert_to_page(response) 81 | self.object_type = self.current_page.value[ 82 | 0].__class__.__name__ if self.current_page.value else None 83 | page = self.current_page 84 | self._next_link = response.get('odata_next_link', '') if isinstance( 85 | response, dict 86 | ) else getattr(response, 'odata_next_link', '') 87 | self._delta_link = response.get('@odata.deltaLink', '') if isinstance( 88 | response, dict 89 | ) else getattr(response, '@odata.deltaLink', '') 90 | 91 | if page is not None: 92 | self.current_page = page 93 | self.has_next = bool(page.odata_next_link) 94 | self.error_mapping = error_mapping if error_mapping else {} 95 | 96 | def set_headers(self, headers: dict) -> HeadersCollection: 97 | """ 98 | Sets the headers for the HTTP requests. 99 | This method takes a dictionary of headers and adds them to the 100 | existing headers. 101 | Args: 102 | headers (dict): A dictionary of headers to add. The keys are the 103 | header names and the values are the header values. 104 | """ 105 | self.headers.add_all(**headers) 106 | return self.headers 107 | 108 | @property 109 | def delta_link(self): 110 | return self._delta_link 111 | 112 | @property 113 | def next_link(self): 114 | return self._next_link 115 | 116 | def set_request_options(self, request_options: list) -> None: 117 | """ 118 | Sets the request options for the HTTP requests. 119 | Args: 120 | request_options (list): The request options to set. 121 | """ 122 | self.request_options = request_options 123 | 124 | async def iterate(self, callback: Callable) -> None: 125 | """ 126 | Iterates over the pages and applies a callback function to each item. 127 | The iteration stops when there are no more pages or the callback 128 | function returns False. 129 | Args: 130 | callback (Callable): The function to apply to each item. 131 | It should take one argument (the item) and return a boolean. 132 | """ 133 | while True: 134 | keep_iterating = self.enumerate(callback) 135 | if not keep_iterating: 136 | return 137 | next_page = await self.next() 138 | if not next_page: 139 | return 140 | self.current_page = next_page 141 | self.pause_index = 0 142 | 143 | async def next(self) -> Optional[PageResult]: 144 | """ 145 | Fetches the next page of items. 146 | Returns: 147 | dict: The next page of items, or None if there are no more pages. 148 | """ 149 | if self.current_page is not None and not self.current_page.odata_next_link: 150 | return None 151 | response = await self.fetch_next_page() 152 | next_link = response.odata_next_link if response and hasattr( 153 | response, 'odata_next_link' 154 | ) else None 155 | value = response.value if response and hasattr(response, 'value') else None 156 | return PageResult(next_link, value) 157 | 158 | @staticmethod 159 | def convert_to_page(response: Union[T, list, object]) -> PageResult: 160 | """ 161 | Converts a response to a PageResult. 162 | This method extracts the 'value' and 'odata_next_link' from the 163 | response and uses them to create a PageResult. 164 | Args: 165 | response (Union[T, list, object]): The response to convert. It can 166 | be a list, an object, or any other type. 167 | Returns: 168 | PageResult: The PageResult created from the response. 169 | Raises: 170 | ValueError: If the response is None or does not contain a 'value'. 171 | """ 172 | if not response: 173 | raise ValueError('Response cannot be null.') 174 | value = None 175 | if isinstance(response, list): 176 | value = response 177 | elif hasattr(response, 'value'): 178 | value = response.value 179 | elif isinstance(response, object): 180 | value = getattr(response, 'value', []) 181 | if value is None: 182 | raise ValueError('The response does not contain a value.') 183 | parsable_page = response if isinstance(response, dict) else vars(response) 184 | next_link = parsable_page.get('odata_next_link', '') if isinstance( 185 | parsable_page, dict 186 | ) else getattr(parsable_page, 'odata_next_link', '') 187 | 188 | return PageResult(next_link, value) 189 | 190 | async def fetch_next_page(self) -> Optional[Union[T, PageResult]]: 191 | """ 192 | Fetches the next page of items from the server. 193 | Returns: 194 | dict: The response from the server. 195 | Raises: 196 | ValueError: If the current page does not contain a next link. 197 | InvalidURL: If the next link URL could not be parsed. 198 | """ 199 | 200 | next_link = self.current_page.odata_next_link 201 | if not next_link: 202 | raise ValueError('The response does not contain a nextLink.') 203 | if not next_link.startswith('http'): 204 | raise InvalidURL('Could not parse nextLink URL.') 205 | request_info = RequestInformation() 206 | request_info.http_method = Method.GET 207 | request_info.url = next_link 208 | request_info.headers = self.headers 209 | if self.request_options: 210 | request_info.add_request_options(*self.request_options) 211 | return await self.request_adapter.send_async( 212 | request_info, 213 | self.parsable_factory, # type: ignore 214 | self.error_mapping 215 | ) 216 | 217 | def enumerate(self, callback: Optional[Callable] = None) -> bool: 218 | """ 219 | Enumerates over the items in the current page and applies a 220 | callback function to each item. 221 | Args: 222 | callback (Callable, optional): The function to apply to each item. 223 | It should take one argument (the item) and return a boolean. 224 | Returns: 225 | bool: False if there are no items in the current page or the 226 | callback function returns False, True otherwise. 227 | """ 228 | keep_iterating = True 229 | page_items = self.current_page.value 230 | if not page_items: 231 | return False 232 | for i in range(self.pause_index, len(page_items)): 233 | keep_iterating = callback(page_items[i]) if callback is not None else True 234 | if not keep_iterating: 235 | self.pause_index = i + 1 236 | break 237 | return keep_iterating 238 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | -------------------------------------------------------------------------------- /tests/authentication/test_azure_identity_authentication_provider.py: -------------------------------------------------------------------------------- 1 | from azure.identity import EnvironmentCredential 2 | from kiota_abstractions.authentication import AuthenticationProvider 3 | 4 | from msgraph_core.authentication import AzureIdentityAuthenticationProvider 5 | 6 | 7 | def test_subclassing(): 8 | assert issubclass(AzureIdentityAuthenticationProvider, AuthenticationProvider) 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | from kiota_abstractions.authentication import AnonymousAuthenticationProvider 4 | 5 | from msgraph_core import APIVersion, NationalClouds 6 | from msgraph_core.graph_client_factory import GraphClientFactory 7 | from msgraph_core.middleware import GraphRequestContext 8 | 9 | BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 10 | 11 | 12 | class MockAuthenticationProvider(AnonymousAuthenticationProvider): 13 | 14 | async def get_authorization_token(self, request: httpx.Request) -> str: 15 | """Returns a string representing a dummy token 16 | Args: 17 | request (GraphRequest): Graph request object 18 | """ 19 | request.headers['Authorization'] = 'Sample token' 20 | return 21 | 22 | 23 | @pytest.fixture 24 | def mock_auth_provider(): 25 | return MockAuthenticationProvider() 26 | 27 | 28 | @pytest.fixture 29 | def mock_transport(): 30 | client = GraphClientFactory.create_with_default_middleware() 31 | return client._transport 32 | 33 | 34 | @pytest.fixture 35 | def mock_request(): 36 | req = httpx.Request('GET', "https://example.org") 37 | req.options = {} 38 | return req 39 | 40 | 41 | @pytest.fixture 42 | def mock_graph_request(): 43 | req = httpx.Request('GET', BASE_URL) 44 | req.context = GraphRequestContext({}, req.headers) 45 | return req 46 | 47 | 48 | @pytest.fixture 49 | def mock_response(): 50 | return httpx.Response( 51 | json={'message': 'Success!'}, status_code=200, headers={"Content-Type": "application/json"} 52 | ) 53 | -------------------------------------------------------------------------------- /tests/middleware/options/test_graph_telemetry_handler_options.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.msgraph_core._constants import SDK_VERSION 4 | from src.msgraph_core._enums import APIVersion 5 | from src.msgraph_core.middleware.options import GraphTelemetryHandlerOption 6 | 7 | 8 | def test_graph_telemetry_handler_options_default(): 9 | telemetry_options = GraphTelemetryHandlerOption() 10 | 11 | assert telemetry_options.get_key() == "GraphTelemetryHandlerOption" 12 | assert telemetry_options.api_version is None 13 | assert telemetry_options.sdk_version == SDK_VERSION 14 | 15 | 16 | def test_graph_telemetry_handler_options_custom(): 17 | telemetry_options = GraphTelemetryHandlerOption( 18 | api_version=APIVersion.beta, sdk_version='1.0.0' 19 | ) 20 | 21 | assert telemetry_options.get_key() == "GraphTelemetryHandlerOption" 22 | assert telemetry_options.api_version == APIVersion.beta 23 | assert telemetry_options.sdk_version == '1.0.0' 24 | -------------------------------------------------------------------------------- /tests/middleware/test_async_graph_transport.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from kiota_http.kiota_client_factory import KiotaClientFactory 3 | 4 | from msgraph_core._enums import FeatureUsageFlag 5 | from msgraph_core.middleware import AsyncGraphTransport, GraphRequestContext 6 | 7 | 8 | def test_set_request_context_and_feature_usage(mock_request, mock_transport): 9 | middleware = KiotaClientFactory.get_default_middleware(None) 10 | pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, mock_transport) 11 | transport = AsyncGraphTransport(mock_transport, pipeline) 12 | transport.set_request_context_and_feature_usage(mock_request) 13 | 14 | assert hasattr(mock_request, 'context') 15 | assert isinstance(mock_request.context, GraphRequestContext) 16 | assert mock_request.context.feature_usage == hex( 17 | FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED 18 | ) 19 | -------------------------------------------------------------------------------- /tests/middleware/test_graph_telemetry_handler.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | import platform 6 | import re 7 | import uuid 8 | 9 | import httpx 10 | import pytest 11 | 12 | from msgraph_core import SDK_VERSION, APIVersion, NationalClouds 13 | from msgraph_core.middleware import GraphRequestContext, GraphTelemetryHandler 14 | from msgraph_core.middleware.options import GraphTelemetryHandlerOption 15 | 16 | BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 17 | 18 | 19 | def test_is_graph_url(mock_graph_request): 20 | """ 21 | Test method that checks whether a request url is a graph endpoint 22 | """ 23 | telemetry_handler = GraphTelemetryHandler() 24 | assert telemetry_handler.is_graph_url(mock_graph_request.url) 25 | 26 | 27 | def test_is_not_graph_url(mock_request): 28 | """ 29 | Test method that checks whether a request url is a graph endpoint with a 30 | non-graph url. 31 | """ 32 | telemetry_handler = GraphTelemetryHandler() 33 | assert not telemetry_handler.is_graph_url(mock_request.url) 34 | 35 | 36 | def test_add_client_request_id_header(mock_graph_request): 37 | """ 38 | Test that client_request_id is added to the request headers 39 | """ 40 | telemetry_handler = GraphTelemetryHandler() 41 | telemetry_handler._add_client_request_id_header(mock_graph_request) 42 | 43 | assert 'client-request-id' in mock_graph_request.headers 44 | assert _is_valid_uuid(mock_graph_request.headers.get('client-request-id')) 45 | 46 | 47 | def test_custom_client_request_id_header(): 48 | """ 49 | Test that a custom client request id is used, if provided 50 | """ 51 | custom_id = str(uuid.uuid4()) 52 | request = httpx.Request('GET', BASE_URL) 53 | request.context = GraphRequestContext({}, {'client-request-id': custom_id}) 54 | 55 | telemetry_handler = GraphTelemetryHandler() 56 | telemetry_handler._add_client_request_id_header(request) 57 | 58 | assert 'client-request-id' in request.headers 59 | assert _is_valid_uuid(request.headers.get('client-request-id')) 60 | assert request.headers.get('client-request-id') == custom_id 61 | 62 | 63 | def test_append_sdk_version_header(mock_graph_request): 64 | """ 65 | Test that sdkVersion is added to the request headers 66 | """ 67 | telemetry_handler = GraphTelemetryHandler() 68 | telemetry_handler._append_sdk_version_header(mock_graph_request, telemetry_handler.options) 69 | 70 | assert 'sdkVersion' in mock_graph_request.headers 71 | assert mock_graph_request.headers.get('sdkVersion' 72 | ).startswith('graph-python-core/' + SDK_VERSION) 73 | 74 | 75 | def test_append_sdk_version_header_beta(mock_graph_request): 76 | """ 77 | Test that sdkVersion is added to the request headers 78 | """ 79 | telemetry_options = GraphTelemetryHandlerOption( 80 | api_version=APIVersion.beta, sdk_version='1.0.0' 81 | ) 82 | telemetry_handler = GraphTelemetryHandler(options=telemetry_options) 83 | telemetry_handler._append_sdk_version_header(mock_graph_request, telemetry_options) 84 | 85 | assert 'sdkVersion' in mock_graph_request.headers 86 | assert mock_graph_request.headers.get('sdkVersion').startswith('graph-python-beta/' + '1.0.0') 87 | 88 | 89 | def test_add_host_os_header(mock_graph_request): 90 | """ 91 | Test that HostOs is added to the request headers 92 | """ 93 | system = platform.system() 94 | version = platform.version() 95 | host_os = f'{system} {version}' 96 | 97 | telemetry_handler = GraphTelemetryHandler() 98 | telemetry_handler._add_host_os_header(mock_graph_request) 99 | 100 | assert 'HostOs' in mock_graph_request.headers 101 | assert mock_graph_request.headers.get('HostOs') == host_os 102 | 103 | 104 | def test_add_runtime_environment_header(mock_graph_request): 105 | """ 106 | Test that RuntimeEnvironment is added to the request headers 107 | """ 108 | python_version = platform.python_version() 109 | runtime_environment = f'Python/{python_version}' 110 | 111 | telemetry_handler = GraphTelemetryHandler() 112 | telemetry_handler._add_runtime_environment_header(mock_graph_request) 113 | 114 | assert 'RuntimeEnvironment' in mock_graph_request.headers 115 | assert mock_graph_request.headers.get('RuntimeEnvironment') == runtime_environment 116 | 117 | 118 | def _is_valid_uuid(guid): 119 | regex = "^[{]?[0-9a-fA-F]{8}" + "-([0-9a-fA-F]{4}-)" + "{3}[0-9a-fA-F]{12}[}]?$" 120 | pattern = re.compile(regex) 121 | if re.search(pattern, guid): 122 | return True 123 | return False 124 | -------------------------------------------------------------------------------- /tests/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-python-core/849b14ae70ae509f12fe1f983db859f099335580/tests/requests/__init__.py -------------------------------------------------------------------------------- /tests/requests/test_batch_request_content.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from io import BytesIO 3 | from unittest.mock import Mock 4 | from urllib.request import Request 5 | from kiota_abstractions.request_information import RequestInformation 6 | from kiota_abstractions.serialization import SerializationWriter 7 | from msgraph_core.requests.batch_request_item import BatchRequestItem 8 | from msgraph_core.requests.batch_request_content import BatchRequestContent 9 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 10 | from msgraph_core.requests.batch_request_item import BatchRequestItem 11 | 12 | 13 | @pytest.fixture 14 | def request_info1(): 15 | request_info = RequestInformation() 16 | request_info.http_method = "GET" 17 | request_info.url = "https://graph.microsoft.com/v1.0/me" 18 | request_info.headers = RequestHeaders() 19 | request_info.headers.add("Content-Type", "application/json") 20 | request_info.content = BytesIO(b'{"key": "value"}') 21 | return request_info 22 | 23 | 24 | @pytest.fixture 25 | def request_info2(): 26 | request_info = RequestInformation() 27 | request_info.http_method = "POST" 28 | request_info.url = "https://graph.microsoft.com/v1.0/users" 29 | request_info.headers = RequestHeaders() 30 | request_info.headers.add("Content-Type", "application/json") 31 | request_info.content = BytesIO(b'{"key": "value"}') 32 | return request_info 33 | 34 | 35 | @pytest.fixture 36 | def batch_request_item1(request_info1): 37 | return BatchRequestItem(request_information=request_info1) 38 | 39 | 40 | @pytest.fixture 41 | def batch_request_item2(request_info2): 42 | return BatchRequestItem(request_information=request_info2) 43 | 44 | 45 | @pytest.fixture 46 | def batch_request_content(batch_request_item1, batch_request_item2): 47 | return BatchRequestContent( 48 | { 49 | batch_request_item1.id: batch_request_item1, 50 | batch_request_item2.id: batch_request_item2 51 | } 52 | ) 53 | 54 | 55 | def test_initialization(batch_request_content, batch_request_item1, batch_request_item2): 56 | assert len(batch_request_content.requests) == 2 57 | 58 | 59 | def test_requests_property(batch_request_content, batch_request_item1, batch_request_item2): 60 | new_request_item = batch_request_item1 61 | batch_request_content.requests = [batch_request_item1, batch_request_item2, new_request_item] 62 | assert len(batch_request_content.requests) == 2 63 | assert batch_request_content.requests[batch_request_item1.id] == new_request_item 64 | 65 | 66 | def test_add_request(batch_request_content, batch_request_item1): 67 | new_request_item = request_info1 68 | new_request_item.id = "new_id" 69 | batch_request_content.add_request(new_request_item.id, new_request_item) 70 | assert len(batch_request_content.requests) == 3 71 | assert batch_request_content.requests[new_request_item.id] == new_request_item 72 | 73 | 74 | def test_add_request_information(batch_request_content): 75 | new_request_info = RequestInformation() 76 | new_request_info.http_method = "DELETE" 77 | new_request_info.url = "https://graph.microsoft.com/v1.0/groups" 78 | batch_request_content.add_request_information(new_request_info) 79 | assert len(batch_request_content.requests) == 3 80 | 81 | 82 | def test_add_urllib_request(batch_request_content): 83 | urllib_request = Request("https://graph.microsoft.com/v1.0/me", method="PATCH") 84 | urllib_request.add_header("Content-Type", "application/json") 85 | urllib_request.data = b'{"key": "value"}' 86 | batch_request_content.add_urllib_request(urllib_request) 87 | assert len(batch_request_content.requests) == 3 88 | 89 | 90 | def test_finalize(batch_request_content): 91 | finalized_batch_request_content = batch_request_content.finalize() 92 | assert batch_request_content.is_finalized 93 | assert finalized_batch_request_content.requests == batch_request_content.requests 94 | 95 | 96 | def test_create_from_discriminator_value(): 97 | parse_node = Mock() 98 | batch_request_content = BatchRequestContent.create_from_discriminator_value(parse_node) 99 | assert isinstance(batch_request_content, BatchRequestContent) 100 | 101 | 102 | def test_get_field_deserializers(batch_request_content): 103 | deserializers = batch_request_content.get_field_deserializers() 104 | assert isinstance(deserializers, dict) 105 | 106 | 107 | def test_serialize(batch_request_content): 108 | writer = Mock(spec=SerializationWriter) 109 | batch_request_content.serialize(writer) 110 | writer.write_collection_of_object_values.assert_called_once_with( 111 | "requests", list(batch_request_content.requests.values()) 112 | ) 113 | -------------------------------------------------------------------------------- /tests/requests/test_batch_request_content_collection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from io import BytesIO 3 | from kiota_abstractions.request_information import RequestInformation 4 | from msgraph_core.requests.batch_request_item import BatchRequestItem 5 | from msgraph_core.requests.batch_request_content_collection import BatchRequestContentCollection 6 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 7 | 8 | 9 | @pytest.fixture 10 | def batch_request_content_collection(): 11 | return BatchRequestContentCollection() 12 | 13 | 14 | @pytest.fixture 15 | def request_info(): 16 | request_info = RequestInformation() 17 | request_info.http_method = "GET" 18 | request_info.url = "https://graph.microsoft.com/v1.0/me" 19 | request_info.headers = RequestHeaders() 20 | request_info.headers.add("Content-Type", "application/json") 21 | request_info.content = BytesIO(b'{"key": "value"}') 22 | return request_info 23 | 24 | 25 | @pytest.fixture 26 | def batch_request_item1(request_info): 27 | return BatchRequestItem(request_information=request_info, id="1") 28 | 29 | 30 | @pytest.fixture 31 | def batch_request_item2(request_info): 32 | return BatchRequestItem(request_information=request_info, id="2") 33 | 34 | 35 | def test_init_batches(batch_request_content_collection): 36 | assert len(batch_request_content_collection.batches) == 1 37 | assert batch_request_content_collection.current_batch is not None 38 | 39 | 40 | def test_add_batch_request_item(batch_request_content_collection, batch_request_item1, batch_request_item2): 41 | batch_request_content_collection.add_batch_request_item(batch_request_item1) 42 | batch_request_content_collection.add_batch_request_item(batch_request_item2) 43 | assert len(batch_request_content_collection.batches) == 1 44 | assert len(batch_request_content_collection.current_batch.requests) == 2 45 | -------------------------------------------------------------------------------- /tests/requests/test_batch_request_item.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from io import BytesIO 3 | from urllib.request import Request 4 | from kiota_abstractions.request_information import RequestInformation 5 | from kiota_abstractions.method import Method 6 | from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders 7 | from msgraph_core.requests.batch_request_item import BatchRequestItem 8 | 9 | base_url = "https://graph.microsoft.com/v1.0/me" 10 | 11 | 12 | @pytest.fixture 13 | def request_info(): 14 | request_info = RequestInformation() 15 | request_info.http_method = "GET" 16 | request_info.url = "f{base_url}/me" 17 | request_info.headers = RequestHeaders() 18 | request_info.content = BytesIO(b'{"key": "value"}') 19 | return request_info 20 | 21 | 22 | @pytest.fixture 23 | def batch_request_item(request_info): 24 | return BatchRequestItem(request_information=request_info) 25 | 26 | 27 | def test_initialization(batch_request_item, request_info): 28 | assert batch_request_item.method == "GET" 29 | assert batch_request_item.url == "f{base_url}/me" 30 | assert batch_request_item.body.read() == b'{"key": "value"}' 31 | 32 | 33 | def test_create_with_urllib_request(): 34 | urllib_request = Request("https://graph.microsoft.com/v1.0/me", method="POST") 35 | urllib_request.add_header("Content-Type", "application/json") 36 | urllib_request.data = b'{"key": "value"}' 37 | batch_request_item = BatchRequestItem.create_with_urllib_request(urllib_request) 38 | assert batch_request_item.method == "POST" 39 | assert batch_request_item.url == "https://graph.microsoft.com/v1.0/me" 40 | assert batch_request_item.body == b'{"key": "value"}' 41 | 42 | 43 | def test_set_depends_on(batch_request_item): 44 | batch_request_item.set_depends_on(["request1", "request2"]) 45 | assert batch_request_item.depends_on == ["request1", "request2"] 46 | 47 | 48 | def test_set_url(batch_request_item): 49 | batch_request_item.set_url("https://graph.microsoft.com/v1.0/me") 50 | assert batch_request_item.url == "/v1.0/me" 51 | 52 | 53 | def test_constructor_url_replacement(): 54 | request_info = RequestInformation() 55 | request_info.http_method = "GET" 56 | request_info.url = "https://graph.microsoft.com/v1.0/users/me-token-to-replace" 57 | request_info.headers = RequestHeaders() 58 | request_info.content = None 59 | 60 | batch_request_item = BatchRequestItem(request_info) 61 | 62 | assert batch_request_item.url == "https://graph.microsoft.com/v1.0/me" 63 | 64 | 65 | def test_set_url_replacement(): 66 | request_info = RequestInformation() 67 | request_info.http_method = "GET" 68 | request_info.url = "https://graph.microsoft.com/v1.0/users/me-token-to-replace" 69 | request_info.headers = RequestHeaders() 70 | request_info.content = None 71 | 72 | batch_request_item = BatchRequestItem(request_info) 73 | batch_request_item.set_url("https://graph.microsoft.com/v1.0/users/me-token-to-replace") 74 | 75 | assert batch_request_item.url == "/v1.0/me" 76 | 77 | 78 | def test_constructor_url_replacement_with_query(): 79 | request_info = RequestInformation() 80 | request_info.http_method = "GET" 81 | request_info.url = "https://graph.microsoft.com/v1.0/users/me-token-to-replace?param=value" 82 | request_info.headers = RequestHeaders() 83 | request_info.content = None 84 | 85 | batch_request_item = BatchRequestItem(request_info) 86 | 87 | assert batch_request_item.url == "https://graph.microsoft.com/v1.0/me?param=value" 88 | 89 | 90 | def test_id_property(batch_request_item): 91 | batch_request_item.id = "new_id" 92 | assert batch_request_item.id == "new_id" 93 | 94 | 95 | def test_headers_property(batch_request_item): 96 | new_headers = {"Authorization": "Bearer token"} 97 | batch_request_item.headers = new_headers 98 | assert batch_request_item.headers["authorization"] == "Bearer token" 99 | 100 | 101 | def test_body_property(batch_request_item): 102 | new_body = BytesIO(b'{"new_key": "new_value"}') 103 | batch_request_item.body = new_body 104 | assert batch_request_item.body == b'{"new_key": "new_value"}' 105 | 106 | 107 | def test_method_property(batch_request_item): 108 | batch_request_item.method = "POST" 109 | assert batch_request_item.method == "POST" 110 | 111 | 112 | def test_batch_request_item_method_enum(): 113 | # Create a RequestInformation instance with an enum value for http_method 114 | request_info = RequestInformation() 115 | request_info.http_method = Method.GET 116 | request_info.url = "https://graph.microsoft.com/v1.0/me" 117 | request_info.headers = RequestHeaders() 118 | request_info.content = None 119 | batch_request_item = BatchRequestItem(request_information=request_info) 120 | assert batch_request_item.method == "GET" 121 | 122 | 123 | def test_depends_on_property(batch_request_item): 124 | batch_request_item.set_depends_on(["request1", "request2"]) 125 | assert batch_request_item.depends_on == ["request1", "request2"] 126 | -------------------------------------------------------------------------------- /tests/requests/test_batch_response_content.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import Mock 3 | from io import BytesIO 4 | from kiota_abstractions.serialization import ParseNode, SerializationWriter, Parsable, ParseNodeFactoryRegistry 5 | from msgraph_core.requests.batch_response_item import BatchResponseItem 6 | from msgraph_core.requests.batch_response_content import BatchResponseContent 7 | 8 | 9 | @pytest.fixture 10 | def batch_response_content(): 11 | return BatchResponseContent() 12 | 13 | 14 | def test_initialization(batch_response_content): 15 | assert batch_response_content.responses == {} 16 | assert isinstance(batch_response_content._responses, dict) 17 | 18 | 19 | def test_responses_property(batch_response_content): 20 | response_item = Mock(spec=BatchResponseItem) 21 | batch_response_content.responses = [response_item] 22 | assert batch_response_content.responses == [response_item] 23 | 24 | 25 | def test_response_method(batch_response_content): 26 | response_item = Mock(spec=BatchResponseItem) 27 | response_item.request_id = "12345" 28 | batch_response_content.responses = {"12345": response_item} 29 | assert batch_response_content.get_response_by_id("12345") == response_item 30 | 31 | 32 | def test_get_response_stream_by_id_none(batch_response_content): 33 | batch_response_content.get_response_by_id = Mock(return_value=None) 34 | result = batch_response_content.get_response_stream_by_id('1') 35 | assert result is None 36 | 37 | 38 | def test_get_response_stream_by_id_body_none(batch_response_content): 39 | batch_response_content.get_response_by_id = Mock(return_value=Mock(body=None)) 40 | result = batch_response_content.get_response_stream_by_id('1') 41 | assert result is None 42 | 43 | 44 | def test_get_response_stream_by_id_bytesio(batch_response_content): 45 | batch_response_content.get_response_by_id = Mock( 46 | return_value=Mock(body=BytesIO(b'Hello, world!')) 47 | ) 48 | result = batch_response_content.get_response_stream_by_id('2') 49 | assert isinstance(result, BytesIO) 50 | assert result.read() == b'Hello, world!' 51 | 52 | 53 | def test_get_response_stream_by_id_bytes(batch_response_content): 54 | batch_response_content.get_response_by_id = Mock(return_value=Mock(body=b'Hello, world!')) 55 | result = batch_response_content.get_response_stream_by_id('1') 56 | assert isinstance(result, BytesIO) 57 | assert result.read() == b'Hello, world!' 58 | 59 | 60 | def test_get_response_status_codes_none(batch_response_content): 61 | batch_response_content._responses = None 62 | result = batch_response_content.get_response_status_codes() 63 | assert result == {} 64 | 65 | 66 | def test_get_response_status_codes(batch_response_content): 67 | batch_response_content._responses = { 68 | '1': Mock(status=200), 69 | '2': Mock(status=404), 70 | '3': Mock(status=500), 71 | } 72 | result = batch_response_content.get_response_status_codes() 73 | expected = { 74 | '1': 200, 75 | '2': 404, 76 | '3': 500, 77 | } 78 | assert result == expected 79 | 80 | 81 | def test_response_body_method(batch_response_content): 82 | response_item = Mock(spec=BatchResponseItem) 83 | response_item.request_id = "12345" 84 | response_item.content_type = "application/json" 85 | response_item.body = BytesIO(b'{"key": "value"}') 86 | batch_response_content.responses = [response_item] 87 | 88 | parse_node = Mock(spec=ParseNode) 89 | parse_node.get_object_value.return_value = {"key": "value"} 90 | registry = Mock(spec=ParseNodeFactoryRegistry) 91 | registry.get_root_parse_node.return_value = parse_node 92 | 93 | with pytest.raises(ValueError): 94 | batch_response_content.response_body("12345", dict) 95 | 96 | 97 | def test_get_field_deserializers(batch_response_content): 98 | deserializers = batch_response_content.get_field_deserializers() 99 | assert isinstance(deserializers, dict) 100 | assert "responses" in deserializers 101 | 102 | 103 | def test_serialize(batch_response_content): 104 | writer = Mock(spec=SerializationWriter) 105 | response_item = Mock(spec=BatchResponseItem) 106 | batch_response_content.responses = {"12345": response_item} 107 | batch_response_content.serialize(writer) 108 | writer.write_collection_of_object_values.assert_called_once_with('responses', [response_item]) 109 | 110 | 111 | def test_create_from_discriminator_value(): 112 | parse_node = Mock(spec=ParseNode) 113 | batch_response_content = BatchResponseContent.create_from_discriminator_value(parse_node) 114 | assert isinstance(batch_response_content, BatchResponseContent) 115 | -------------------------------------------------------------------------------- /tests/requests/test_batch_response_item.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from io import BytesIO 3 | 4 | from kiota_abstractions.serialization import ParseNode, SerializationWriter 5 | from unittest.mock import Mock 6 | 7 | from msgraph_core.requests.batch_response_item import BatchResponseItem 8 | 9 | 10 | @pytest.fixture 11 | def batch_response_item(): 12 | return BatchResponseItem() 13 | 14 | 15 | def test_initialization(batch_response_item): 16 | assert batch_response_item.id is None 17 | assert batch_response_item.atomicity_group is None 18 | assert batch_response_item.status is None 19 | assert batch_response_item.headers == {} 20 | assert batch_response_item.body is None 21 | 22 | 23 | def test_id_property(batch_response_item): 24 | batch_response_item.id = "12345" 25 | assert batch_response_item.id == "12345" 26 | 27 | 28 | def test_atomicity_group_property(batch_response_item): 29 | batch_response_item.atomicity_group = "group1" 30 | assert batch_response_item.atomicity_group == "group1" 31 | 32 | 33 | def test_status_property(batch_response_item): 34 | batch_response_item.status = 200 35 | assert batch_response_item.status == 200 36 | 37 | 38 | def test_headers_property(batch_response_item): 39 | headers = {"Content-Type": "application/json"} 40 | batch_response_item.headers = headers 41 | assert batch_response_item.headers == headers 42 | 43 | 44 | def test_body_property(batch_response_item): 45 | body = BytesIO(b"response body") 46 | batch_response_item.body = body 47 | assert batch_response_item.body == body 48 | 49 | 50 | def test_content_type_property(batch_response_item): 51 | headers = {"Content-Type": "application/json"} 52 | batch_response_item.headers = headers 53 | assert batch_response_item.content_type == "application/json" 54 | 55 | 56 | def test_create_from_discriminator_value(): 57 | parse_node = Mock(spec=ParseNode) 58 | batch_response_item = BatchResponseItem.create_from_discriminator_value(parse_node) 59 | assert isinstance(batch_response_item, BatchResponseItem) 60 | 61 | 62 | def test_get_field_deserializers(batch_response_item): 63 | deserializers = batch_response_item.get_field_deserializers() 64 | assert isinstance(deserializers, dict) 65 | assert "id" in deserializers 66 | assert "status" in deserializers 67 | assert "headers" in deserializers 68 | assert "body" in deserializers 69 | 70 | 71 | def test_serialize(batch_response_item): 72 | writer = Mock(spec=SerializationWriter) 73 | batch_response_item.id = "12345" 74 | batch_response_item.atomicity_group = "group1" 75 | batch_response_item.status = 200 76 | batch_response_item.headers = {"Content-Type": "application/json"} 77 | batch_response_item.body = BytesIO(b"response body") 78 | batch_response_item.serialize(writer) 79 | writer.write_str_value.assert_any_call('id', "12345") 80 | writer.write_str_value.assert_any_call('atomicity_group', "group1") 81 | writer.write_int_value.assert_any_call('status', 200) 82 | writer.write_collection_of_primitive_values.assert_any_call( 83 | 'headers', {"Content-Type": "application/json"} 84 | ) 85 | -------------------------------------------------------------------------------- /tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-python-core/849b14ae70ae509f12fe1f983db859f099335580/tests/tasks/__init__.py -------------------------------------------------------------------------------- /tests/tasks/test_page_iterator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import AsyncMock, patch, Mock 3 | 4 | import pytest 5 | from azure.identity import ClientSecretCredential 6 | from kiota_authentication_azure.azure_identity_authentication_provider\ 7 | import AzureIdentityAuthenticationProvider 8 | from kiota_http.httpx_request_adapter import HttpxRequestAdapter 9 | from dotenv import load_dotenv 10 | 11 | from msgraph_core.tasks.page_iterator import PageIterator # pylint: disable=import-error, no-name-in-module 12 | from msgraph_core.models.page_result import PageResult # pylint: disable=no-name-in-module, import-error 13 | 14 | 15 | @pytest.fixture 16 | def first_page_data(): 17 | return { 18 | "@odata.context": 19 | "https://graph.microsoft.com/v1.0/$metadata#users", 20 | "@odata.next_link": 21 | "https://graph.microsoft.com/v1.0/users?skip=2&page=10", 22 | "value": [ 23 | { 24 | "businessPhones": [], 25 | "displayName": "Conf Room Adams 1", 26 | "givenName": None, 27 | "jobTitle": None, 28 | "mail": "Adams@contoso.com", 29 | "mobilePhone": None, 30 | "officeLocation": None, 31 | "preferredLanguage": None, 32 | "surname": None, 33 | "userPrincipalName": "Adams@contoso.com", 34 | "id": "6ea91a8d-e32e-41a1-b7bd-d2d185eed0e0" 35 | }, { 36 | "businessPhones": ["425-555-0100"], 37 | "displayName": "MOD Administrator 1", 38 | "givenName": "MOD", 39 | "jobTitle": None, 40 | "mail": None, 41 | "mobilePhone": "425-555-0101", 42 | "officeLocation": None, 43 | "preferredLanguage": "en-US", 44 | "surname": "Administrator", 45 | "userPrincipalName": "admin@contoso.com", 46 | "id": "4562bcc8-c436-4f95-b7c0-4f8ce89dca5e" 47 | } 48 | ] 49 | } 50 | 51 | 52 | @pytest.fixture 53 | def second_page_data(): 54 | return { 55 | "@odata.context": 56 | "https://graph.microsoft.com/v1.0/$metadata#users", 57 | "value": [ 58 | { 59 | "businessPhones": [], 60 | "displayName": "Conf Room Adams 2", 61 | "givenName": None, 62 | "jobTitle": None, 63 | "mail": "Adams@contoso.com", 64 | "mobilePhone": None, 65 | "officeLocation": None, 66 | "preferredLanguage": None, 67 | "surname": None, 68 | "userPrincipalName": "Adams@contoso.com", 69 | "id": "6ea91a8d-e32e-41a1-b7bd-d2d185eed0e0" 70 | }, { 71 | "businessPhones": ["425-555-0100"], 72 | "displayName": "MOD Administrator 2", 73 | "givenName": "MOD", 74 | "jobTitle": None, 75 | "mail": None, 76 | "mobilePhone": "425-555-0101", 77 | "officeLocation": None, 78 | "preferredLanguage": "en-US", 79 | "surname": "Administrator", 80 | "userPrincipalName": "admin@contoso.com", 81 | "id": "4562bcc8-c436-4f95-b7c0-4f8ce89dca5e" 82 | } 83 | ] 84 | } 85 | 86 | 87 | credential = Mock() 88 | auth_provider = Mock() 89 | request_adapter = Mock() 90 | 91 | 92 | def test_convert_to_page(first_page_data): # pylint: disable=redefined-outer-name 93 | 94 | page_iterator = PageIterator(first_page_data, request_adapter) 95 | first_page = page_iterator.convert_to_page(first_page_data) 96 | first_page.value = first_page_data['value'] 97 | first_page.odata_next_link = first_page_data['@odata.next_link'] 98 | assert isinstance(first_page, PageResult) 99 | assert first_page_data['value'] == first_page.value 100 | assert first_page_data['@odata.next_link'] == first_page.odata_next_link 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_iterate(): 105 | # Mock the next method to return None after the first call 106 | with patch.object(PageIterator, 'next', new_callable=AsyncMock) as mock_next: 107 | mock_next.side_effect = [True, None] 108 | 109 | with patch.object(PageIterator, 'enumerate', return_value=True) as mock_enumerate: 110 | page_iterator = PageIterator(first_page_data, request_adapter) 111 | await page_iterator.iterate(lambda _: True) 112 | assert mock_next.call_count == 2 113 | assert mock_enumerate.call_count == 2 114 | -------------------------------------------------------------------------------- /tests/tasks/test_page_result.py: -------------------------------------------------------------------------------- 1 | from msgraph_core.models import PageResult # pylint: disable=no-name-in-module, import-error 2 | 3 | 4 | def test_initialization(): 5 | page_result = PageResult() 6 | assert page_result.odata_next_link is None 7 | assert page_result.value is None 8 | 9 | 10 | def test_set_and_get_values(): 11 | page_result = PageResult() 12 | page_result.value = [{"name": "John Doe"}, {"name": "Ian Smith"}] 13 | page_result.odata_next_link = "next_page" 14 | assert 2 == len(page_result.value) 15 | assert "next_page" == page_result.odata_next_link 16 | assert [{"name": "John Doe"}, {"name": "Ian Smith"}] == page_result.value 17 | -------------------------------------------------------------------------------- /tests/test_base_graph_request_adapter.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | from kiota_abstractions.serialization import ( 4 | ParseNodeFactoryRegistry, 5 | SerializationWriterFactoryRegistry, 6 | ) 7 | 8 | from msgraph_core.base_graph_request_adapter import BaseGraphRequestAdapter 9 | 10 | 11 | def test_create_graph_request_adapter(mock_auth_provider): 12 | request_adapter = BaseGraphRequestAdapter(mock_auth_provider) 13 | assert request_adapter._authentication_provider is mock_auth_provider 14 | assert isinstance(request_adapter._parse_node_factory, ParseNodeFactoryRegistry) 15 | assert isinstance( 16 | request_adapter._serialization_writer_factory, SerializationWriterFactoryRegistry 17 | ) 18 | assert isinstance(request_adapter._http_client, httpx.AsyncClient) 19 | assert request_adapter.base_url == 'https://graph.microsoft.com/v1.0/' 20 | 21 | 22 | def test_create_request_adapter_no_auth_provider(): 23 | with pytest.raises(TypeError): 24 | BaseGraphRequestAdapter(None) 25 | -------------------------------------------------------------------------------- /tests/test_graph_client_factory.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------ 2 | # Copyright (c) Microsoft Corporation. 3 | # Licensed under the MIT License. 4 | # ------------------------------------ 5 | import httpx 6 | import pytest 7 | from kiota_http.middleware import MiddlewarePipeline, RedirectHandler, RetryHandler 8 | from kiota_http.middleware.options import RedirectHandlerOption, RetryHandlerOption 9 | 10 | from msgraph_core import APIVersion, GraphClientFactory, NationalClouds 11 | from msgraph_core.middleware import AsyncGraphTransport, GraphTelemetryHandler 12 | 13 | 14 | def test_create_with_default_middleware(): 15 | """Test creation of GraphClient using default middleware""" 16 | client = GraphClientFactory.create_with_default_middleware() 17 | 18 | assert isinstance(client, httpx.AsyncClient) 19 | assert isinstance(client._transport, AsyncGraphTransport) 20 | pipeline = client._transport.pipeline 21 | assert isinstance(pipeline, MiddlewarePipeline) 22 | assert isinstance(pipeline._first_middleware, RedirectHandler) 23 | assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) 24 | 25 | 26 | def test_create_with_default_middleware_custom_client(): 27 | """Test creation of GraphClient using default middleware""" 28 | timeout = httpx.Timeout(20, connect=10) 29 | custom_client = httpx.AsyncClient(timeout=timeout, http2=True) 30 | client = GraphClientFactory.create_with_default_middleware(client=custom_client) 31 | 32 | assert isinstance(client, httpx.AsyncClient) 33 | assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) 34 | assert isinstance(client._transport, AsyncGraphTransport) 35 | pipeline = client._transport.pipeline 36 | assert isinstance(pipeline, MiddlewarePipeline) 37 | assert isinstance(pipeline._first_middleware, RedirectHandler) 38 | assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) 39 | 40 | 41 | def test_create_with_default_middleware_custom_client_with_proxy(): 42 | """Test creation of GraphClient using default middleware""" 43 | proxies = { 44 | "http://": httpx.HTTPTransport(proxy="http://localhost:8030"), 45 | "https://": httpx.HTTPTransport(proxy="http://localhost:8031"), 46 | } 47 | timeout = httpx.Timeout(20, connect=10) 48 | custom_client = httpx.AsyncClient(timeout=timeout, http2=True, mounts=proxies) 49 | client = GraphClientFactory.create_with_default_middleware(client=custom_client) 50 | 51 | assert isinstance(client, httpx.AsyncClient) 52 | assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) 53 | assert isinstance(client._transport, AsyncGraphTransport) 54 | pipeline = client._transport.pipeline 55 | assert isinstance(pipeline, MiddlewarePipeline) 56 | assert isinstance(pipeline._first_middleware, RedirectHandler) 57 | assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) 58 | assert client._mounts 59 | for pattern, transport in client._mounts.items(): 60 | assert isinstance(transport, AsyncGraphTransport) 61 | 62 | 63 | def test_create_default_with_custom_middleware(): 64 | """Test creation of HTTP Client using default middleware and custom options""" 65 | retry_options = RetryHandlerOption(max_retries=5) 66 | options = {f'{retry_options.get_key()}': retry_options} 67 | client = GraphClientFactory.create_with_default_middleware(options=options) 68 | 69 | assert isinstance(client, httpx.AsyncClient) 70 | assert isinstance(client._transport, AsyncGraphTransport) 71 | pipeline = client._transport.pipeline 72 | assert isinstance(pipeline, MiddlewarePipeline) 73 | assert isinstance(pipeline._first_middleware, RedirectHandler) 74 | retry_handler = pipeline._first_middleware.next 75 | assert isinstance(retry_handler, RetryHandler) 76 | assert retry_handler.options.max_retry == retry_options.max_retry 77 | assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) 78 | 79 | 80 | def test_create_with_custom_middleware_custom_client(): 81 | """Test creation of HTTP Clients with custom middleware""" 82 | timeout = httpx.Timeout(20, connect=10) 83 | custom_client = httpx.AsyncClient(timeout=timeout, http2=True) 84 | middleware = [ 85 | GraphTelemetryHandler(), 86 | ] 87 | client = GraphClientFactory.create_with_custom_middleware( 88 | middleware=middleware, client=custom_client 89 | ) 90 | 91 | assert isinstance(client, httpx.AsyncClient) 92 | assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) 93 | assert isinstance(client._transport, AsyncGraphTransport) 94 | pipeline = client._transport.pipeline 95 | assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) 96 | 97 | 98 | def test_create_with_custom_middleware_custom_client_with_proxy(): 99 | """Test creation of HTTP Clients with custom middleware""" 100 | proxies = { 101 | "http://": httpx.HTTPTransport(proxy="http://localhost:8030"), 102 | "https://": httpx.HTTPTransport(proxy="http://localhost:8031"), 103 | } 104 | timeout = httpx.Timeout(20, connect=10) 105 | custom_client = httpx.AsyncClient(timeout=timeout, http2=True, mounts=proxies) 106 | middleware = [ 107 | GraphTelemetryHandler(), 108 | ] 109 | client = GraphClientFactory.create_with_custom_middleware( 110 | middleware=middleware, client=custom_client 111 | ) 112 | 113 | assert isinstance(client, httpx.AsyncClient) 114 | assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) 115 | assert isinstance(client._transport, AsyncGraphTransport) 116 | pipeline = client._transport.pipeline 117 | assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) 118 | assert client._mounts 119 | for pattern, transport in client._mounts.items(): 120 | assert isinstance(transport, AsyncGraphTransport) 121 | pipeline = transport.pipeline 122 | assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) 123 | 124 | 125 | def test_graph_client_factory_with_custom_configuration(): 126 | """ 127 | Test creating a graph client with custom url overrides the default 128 | """ 129 | graph_client = GraphClientFactory.create_with_default_middleware( 130 | api_version=APIVersion.beta, host=NationalClouds.China 131 | ) 132 | assert isinstance(graph_client, httpx.AsyncClient) 133 | assert str(graph_client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' 134 | 135 | 136 | def test_get_base_url(): 137 | """ 138 | Test base url is formed by combining the national cloud endpoint with 139 | Api version 140 | """ 141 | url = GraphClientFactory._get_base_url( 142 | host=NationalClouds.Germany, 143 | api_version=APIVersion.beta, 144 | ) 145 | assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' 146 | --------------------------------------------------------------------------------