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