{{ error.message }}
75 | {% if error.debug %} 76 |{{ error.debug }}
77 | {% endif %}
78 | ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── ask-a-question.md │ └── bug_report.md ├── dependabot.yml ├── policies │ └── resourceManagement.yml └── workflows │ ├── auto-merge-dependabot.yml │ └── django.yml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md └── graph_tutorial ├── .pylintrc ├── graph_tutorial ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── oauth_settings.example.yml ├── requirements.txt └── tutorial ├── __init__.py ├── admin.py ├── apps.py ├── auth_helper.py ├── graph_helper.py ├── migrations └── __init__.py ├── models.py ├── static └── tutorial │ ├── app.css │ └── no-profile-photo.png ├── templates └── tutorial │ ├── calendar.html │ ├── home.html │ ├── layout.html │ └── newevent.html ├── tests.py ├── urls.py └── views.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ask-a-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Ask a question 3 | about: Ask a question about Graph, adding features to this sample, etc. 4 | title: '' 5 | labels: question, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thank you for taking an interest in Microsoft Graph development! Please feel free to ask a question here, but keep in mind the following: 11 | 12 | - This is not an official Microsoft support channel, and our ability to respond to questions here is limited. Questions about Graph, or questions about adding a new feature to the sample, will be answered on a best-effort basis. 13 | - Questions should be asked on [Stack Overflow](https://stackoverflow.com/questions/tagged/microsoft-graph). 14 | - Issues with Microsoft Graph itself should be handled through [support](https://developer.microsoft.com/graph/support). 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug report, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Dependency versions** 32 | - Authentication library (MSAL, etc.) version: 33 | - Graph library (Graph SDK, REST library, etc.) version: 34 | 35 | **Additional context** 36 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/graph_tutorial/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.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: needs author feedback 20 | - hasLabel: 21 | label: 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: needs author feedback 35 | - noActivitySince: 36 | days: 4 37 | - isNotLabeledWith: 38 | label: no recent activity 39 | actions: 40 | - addLabel: 41 | label: 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 | - description: 60 | frequencies: 61 | - hourly: 62 | hour: 3 63 | filters: 64 | - isOpen 65 | - isIssue 66 | - hasLabel: 67 | label: graph question 68 | actions: 69 | - removeLabel: 70 | label: 'needs attention :wave:' 71 | - removeLabel: 72 | label: needs author feedback 73 | - removeLabel: 74 | label: 'needs triage :mag:' 75 | - removeLabel: 76 | label: no recent activity 77 | - addLabel: 78 | label: out of scope 79 | - addReply: 80 | reply: >- 81 | It looks like you are asking a question about using Microsoft Graph or one of the Microsoft Graph SDKs that is not directly related to this sample. Unfortunately we are not set up to answer general questions in this repository, so this issue will be closed. 82 | 83 | 84 | Please try asking your question on [Stack Overflow](https://stackoverflow.com/questions/tagged/microsoft-graph-api), tagging your question with `microsoft-graph-api`. 85 | - closeIssue 86 | - description: 87 | frequencies: 88 | - hourly: 89 | hour: 3 90 | filters: 91 | - isOpen 92 | - isIssue 93 | - hasLabel: 94 | label: graph issue 95 | actions: 96 | - removeLabel: 97 | label: 'needs attention :wave:' 98 | - removeLabel: 99 | label: needs author feedback 100 | - removeLabel: 101 | label: 'needs triage :mag:' 102 | - removeLabel: 103 | label: no recent activity 104 | - addLabel: 105 | label: out of scope 106 | - addReply: 107 | reply: >- 108 | It looks like you are reporting an issue with Microsoft Graph or one of the Microsoft Graph SDKs that is not fixable by changing code in this sample. Unfortunately we are not set up to provide product support in this repository, so this issue will be closed. 109 | 110 | 111 | Please visit one of the following links to report your issue. 112 | 113 | 114 | - Issue with Microsoft Graph service: [Microsoft Graph support](https://developer.microsoft.com/graph/support#report-issues-with-the-service), choose one of the options under **Report issues with the service** 115 | 116 | - Issue with a Microsoft Graph SDK: Open an issue in the SDK's GitHub repository. See [microsoftgraph on GitHub](https://github.com/microsoftgraph?q=sdk+in%3Aname&type=public&language=) for a list of SDK repositories. 117 | - closeIssue 118 | eventResponderTasks: 119 | - if: 120 | - payloadType: Issue_Comment 121 | - isAction: 122 | action: Created 123 | - isActivitySender: 124 | issueAuthor: True 125 | - hasLabel: 126 | label: needs author feedback 127 | - isOpen 128 | then: 129 | - addLabel: 130 | label: 'needs attention :wave:' 131 | - removeLabel: 132 | label: needs author feedback 133 | description: 134 | - if: 135 | - payloadType: Issues 136 | - not: 137 | isAction: 138 | action: Closed 139 | - hasLabel: 140 | label: no recent activity 141 | then: 142 | - removeLabel: 143 | label: no recent activity 144 | description: 145 | - if: 146 | - payloadType: Issue_Comment 147 | - hasLabel: 148 | label: no recent activity 149 | then: 150 | - removeLabel: 151 | label: no recent activity 152 | description: 153 | - if: 154 | - payloadType: Pull_Request 155 | then: 156 | - inPrLabel: 157 | label: in pr 158 | description: 159 | onFailure: 160 | onSuccess: 161 | -------------------------------------------------------------------------------- /.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 | 13 | dependabot-merge: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | if: ${{ github.actor == 'dependabot[bot]' }} 18 | 19 | steps: 20 | - name: Dependabot metadata 21 | id: metadata 22 | uses: dependabot/fetch-metadata@v2.4.0 23 | with: 24 | github-token: "${{ secrets.GITHUB_TOKEN }}" 25 | 26 | - name: Enable auto-merge for Dependabot PRs 27 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 28 | run: gh pr merge --auto --merge "$PR_URL" 29 | env: 30 | PR_URL: ${{github.event.pull_request.html_url}} 31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | defaults: 13 | run: 14 | working-directory: graph_tutorial 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | max-parallel: 4 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: Copy settings file 25 | run: | 26 | cp oauth_settings.example.yml oauth_settings.yml 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Install Dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | - name: Run Tests 36 | run: | 37 | python manage.py test 38 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # OAuth settings 107 | oauth_settings.yml 108 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.enabled": true, 4 | "python.linting.pylintArgs": [ 5 | "--rcfile=graph_tutorial/.pylintrc" 6 | ], 7 | "cSpell.words": [ 8 | "calendarview", 9 | "dateutil", 10 | "gettz", 11 | "newevent", 12 | "signin", 13 | "signout", 14 | "timespec" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | description: This sample demonstrates how to use the Microsoft Graph to access data in Office 365 from a Python Django app. 4 | products: 5 | - ms-graph 6 | - office-exchange-online 7 | languages: 8 | - python 9 | --- 10 | # Microsoft Graph sample Python web app 11 | 12 | This sample uses Microsoft Graph to access data in Office 365 by building a Python Django web application using the Azure AD v2 authentication endpoint. 13 | 14 | ## Prerequisites 15 | - [Python](https://www.python.org/) (with [pip](https://pypi.org/project/pip/)) installed on your development machine. If you do not have Python, visit the previous link for download options. (**Note:** This tutorial was written with Python version 3.7.9 and Django version 3.2.15 The steps in this guide may work with other versions, but that has not been tested.) 16 | - Either a personal Microsoft account with a mailbox on Outlook.com, or a Microsoft work or school account. 17 | 18 | If you don't have a Microsoft account, there are a couple of options to get a free account: 19 | 20 | - You can [sign up for a new personal Microsoft account](https://signup.live.com/signup?wa=wsignin1.0&rpsnv=12&ct=1454618383&rver=6.4.6456.0&wp=MBI_SSL_SHARED&wreply=https://mail.live.com/default.aspx&id=64855&cbcxt=mai&bk=1454618383&uiflavor=web&uaid=b213a65b4fdc484382b6622b3ecaa547&mkt=E-US&lc=1033&lic=1). 21 | - You can [sign up for the Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) to get a free Office 365 subscription. 22 | 23 | ## Register a web application with the Azure Active Directory admin center 24 | 25 | 1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com). Login using a **personal account** (aka: Microsoft Account) or **Work or School Account**. 26 | 27 | 1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations** under **Manage**. 28 | 29 | 1. Select **New registration**. On the **Register an application** page, set the values as follows. 30 | 31 | - Set **Name** to `Python Graph Tutorial`. 32 | - Set **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts**. 33 | - Under **Redirect URI**, set the first drop-down to `Web` and set the value to `http://localhost:8000/callback`. 34 | 35 | 1. Choose **Register**. On the **Python Graph Tutorial** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step. 36 | 37 | 1. Select **Certificates & secrets** under **Manage**. Select the **New client secret** button. Enter a value in **Description** and select one of the options for **Expires** and choose **Add**. 38 | 39 | 1. Copy the client secret value before you leave this page. You will need it in the next step. 40 | 41 | > [!IMPORTANT] 42 | > This client secret is never shown again, so make sure you copy it now. 43 | 44 | ## Configure the sample 45 | 46 | 1. Rename the `oauth_settings.example.yml` file to `oauth_settings.yml`. 47 | 1. Edit the `oauth_settings.yml` file and make the following changes. 48 | 1. Replace `YOUR_APP_ID_HERE` with the **Application Id** you got from the App Registration Portal. 49 | 1. Replace `YOUR_APP_PASSWORD_HERE` with the password you got from the App Registration Portal. 50 | 1. In your command-line interface (CLI), navigate to this directory and run the following command to install requirements. 51 | 52 | ```Shell 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | 1. In your CLI, run the following command to initialize the app's database. 57 | 58 | ```Shell 59 | python manage.py migrate 60 | ``` 61 | 62 | ## Run the sample 63 | 64 | 1. Run the following command in your CLI to start the application. 65 | 66 | ```Shell 67 | python manage.py runserver 68 | ``` 69 | 70 | 1. Open a browser and browse to `http://localhost:8000`. 71 | 72 | ## Code of conduct 73 | 74 | 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. 75 | 76 | ## Disclaimer 77 | 78 | **THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** 79 | -------------------------------------------------------------------------------- /graph_tutorial/.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold to be exceeded before program exits with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regex patterns to the ignore-list. The 48 | # regex matches against paths and can be in Posix or Windows format. 49 | ignore-paths= 50 | 51 | # Files or directories matching the regex patterns are skipped. The regex 52 | # matches against base names, not paths. The default value ignores Emacs file 53 | # locks 54 | ignore-patterns=^\.# 55 | 56 | # List of module names for which member attributes should not be checked 57 | # (useful for modules/projects where namespaces are manipulated during runtime 58 | # and thus existing member attributes cannot be deduced by static analysis). It 59 | # supports qualified module names, as well as Unix pattern matching. 60 | ignored-modules= 61 | 62 | # Python code to execute, usually for sys.path manipulation such as 63 | # pygtk.require(). 64 | #init-hook= 65 | 66 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 67 | # number of processors available to use, and will cap the count on Windows to 68 | # avoid hangs. 69 | jobs=1 70 | 71 | # Control the amount of potential inferred values when inferring a single 72 | # object. This can help the performance when dealing with large functions or 73 | # complex, nested conditions. 74 | limit-inference-results=100 75 | 76 | # List of plugins (as comma separated values of python module names) to load, 77 | # usually to register additional checkers. 78 | load-plugins= 79 | 80 | # Pickle collected data for later comparisons. 81 | persistent=yes 82 | 83 | # Minimum Python version to use for version dependent checks. Will default to 84 | # the version used to run pylint. 85 | py-version=3.10 86 | 87 | # Discover python modules and packages in the file system subtree. 88 | recursive=no 89 | 90 | # When enabled, pylint would attempt to guess common misconfiguration and emit 91 | # user-friendly hints instead of false-positive error messages. 92 | suggestion-mode=yes 93 | 94 | # Allow loading of arbitrary C extensions. Extensions are imported into the 95 | # active Python interpreter and may run arbitrary code. 96 | unsafe-load-any-extension=no 97 | 98 | # In verbose mode, extra non-checker-related info will be displayed. 99 | #verbose= 100 | 101 | 102 | [REPORTS] 103 | 104 | # Python expression which should return a score less than or equal to 10. You 105 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 106 | # 'convention', and 'info' which contain the number of messages in each 107 | # category, as well as 'statement' which is the total number of statements 108 | # analyzed. This score is used by the global evaluation report (RP0004). 109 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 110 | 111 | # Template used to display messages. This is a python new-style format string 112 | # used to format the message information. See doc for all details. 113 | msg-template= 114 | 115 | # Set the output format. Available formats are text, parseable, colorized, json 116 | # and msvs (visual studio). You can also give a reporter class, e.g. 117 | # mypackage.mymodule.MyReporterClass. 118 | #output-format= 119 | 120 | # Tells whether to display a full report or only the messages. 121 | reports=no 122 | 123 | # Activate the evaluation score. 124 | score=yes 125 | 126 | 127 | [MESSAGES CONTROL] 128 | 129 | # Only show warnings with the listed confidence levels. Leave empty to show 130 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 131 | # UNDEFINED. 132 | confidence=HIGH, 133 | CONTROL_FLOW, 134 | INFERENCE, 135 | INFERENCE_FAILURE, 136 | UNDEFINED 137 | 138 | # Disable the message, report, category or checker with the given id(s). You 139 | # can either give multiple identifiers separated by comma (,) or put this 140 | # option multiple times (only on the command line, not in the configuration 141 | # file where it should appear only once). You can also use "--disable=all" to 142 | # disable everything first and then re-enable specific checks. For example, if 143 | # you want to run only the similarities checker, you can use "--disable=all 144 | # --enable=similarities". If you want to run only the classes checker, but have 145 | # no Warning level messages displayed, use "--disable=all --enable=classes 146 | # --disable=W". 147 | disable=missing-module-docstring, 148 | missing-function-docstring 149 | 150 | # Enable the message, report, category or checker with the given id(s). You can 151 | # either give multiple identifier separated by comma (,) or put this option 152 | # multiple time (only on the command line, not in the configuration file where 153 | # it should appear only once). See also the "--disable" option for examples. 154 | # enable=bad-indentation 155 | 156 | 157 | [EXCEPTIONS] 158 | 159 | # Exceptions that will emit a warning when caught. 160 | overgeneral-exceptions=BaseException, 161 | Exception 162 | 163 | 164 | [CLASSES] 165 | 166 | # Warn about protected attribute access inside special methods 167 | check-protected-access-in-special-methods=no 168 | 169 | # List of method names used to declare (i.e. assign) instance attributes. 170 | defining-attr-methods=__init__, 171 | __new__, 172 | setUp, 173 | __post_init__ 174 | 175 | # List of member names, which should be excluded from the protected access 176 | # warning. 177 | exclude-protected=_asdict, 178 | _fields, 179 | _replace, 180 | _source, 181 | _make 182 | 183 | # List of valid names for the first argument in a class method. 184 | valid-classmethod-first-arg=cls 185 | 186 | # List of valid names for the first argument in a metaclass class method. 187 | valid-metaclass-classmethod-first-arg=cls 188 | 189 | 190 | [STRING] 191 | 192 | # This flag controls whether inconsistent-quotes generates a warning when the 193 | # character used as a quote delimiter is used inconsistently within a module. 194 | check-quote-consistency=no 195 | 196 | # This flag controls whether the implicit-str-concat should generate a warning 197 | # on implicit string concatenation in sequences defined over several lines. 198 | check-str-concat-over-line-jumps=no 199 | 200 | 201 | [MISCELLANEOUS] 202 | 203 | # List of note tags to take in consideration, separated by a comma. 204 | notes=FIXME, 205 | XXX, 206 | TODO 207 | 208 | # Regular expression of note tags to take in consideration. 209 | notes-rgx= 210 | 211 | 212 | [SPELLING] 213 | 214 | # Limits count of emitted suggestions for spelling mistakes. 215 | max-spelling-suggestions=4 216 | 217 | # Spelling dictionary name. Available dictionaries: none. To make it work, 218 | # install the 'python-enchant' package. 219 | spelling-dict= 220 | 221 | # List of comma separated words that should be considered directives if they 222 | # appear at the beginning of a comment and should not be checked. 223 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 224 | 225 | # List of comma separated words that should not be checked. 226 | spelling-ignore-words= 227 | 228 | # A path to a file that contains the private dictionary; one word per line. 229 | spelling-private-dict-file= 230 | 231 | # Tells whether to store unknown words to the private dictionary (see the 232 | # --spelling-private-dict-file option) instead of raising a message. 233 | spelling-store-unknown-words=no 234 | 235 | 236 | [BASIC] 237 | 238 | # Naming style matching correct argument names. 239 | argument-naming-style=snake_case 240 | 241 | # Regular expression matching correct argument names. Overrides argument- 242 | # naming-style. If left empty, argument names will be checked with the set 243 | # naming style. 244 | #argument-rgx= 245 | 246 | # Naming style matching correct attribute names. 247 | attr-naming-style=snake_case 248 | 249 | # Regular expression matching correct attribute names. Overrides attr-naming- 250 | # style. If left empty, attribute names will be checked with the set naming 251 | # style. 252 | #attr-rgx= 253 | 254 | # Bad variable names which should always be refused, separated by a comma. 255 | bad-names=foo, 256 | bar, 257 | baz, 258 | toto, 259 | tutu, 260 | tata 261 | 262 | # Bad variable names regexes, separated by a comma. If names match any regex, 263 | # they will always be refused 264 | bad-names-rgxs= 265 | 266 | # Naming style matching correct class attribute names. 267 | class-attribute-naming-style=any 268 | 269 | # Regular expression matching correct class attribute names. Overrides class- 270 | # attribute-naming-style. If left empty, class attribute names will be checked 271 | # with the set naming style. 272 | #class-attribute-rgx= 273 | 274 | # Naming style matching correct class constant names. 275 | class-const-naming-style=UPPER_CASE 276 | 277 | # Regular expression matching correct class constant names. Overrides class- 278 | # const-naming-style. If left empty, class constant names will be checked with 279 | # the set naming style. 280 | #class-const-rgx= 281 | 282 | # Naming style matching correct class names. 283 | class-naming-style=PascalCase 284 | 285 | # Regular expression matching correct class names. Overrides class-naming- 286 | # style. If left empty, class names will be checked with the set naming style. 287 | #class-rgx= 288 | 289 | # Naming style matching correct constant names. 290 | const-naming-style=UPPER_CASE 291 | 292 | # Regular expression matching correct constant names. Overrides const-naming- 293 | # style. If left empty, constant names will be checked with the set naming 294 | # style. 295 | #const-rgx= 296 | 297 | # Minimum line length for functions/classes that require docstrings, shorter 298 | # ones are exempt. 299 | docstring-min-length=-1 300 | 301 | # Naming style matching correct function names. 302 | function-naming-style=snake_case 303 | 304 | # Regular expression matching correct function names. Overrides function- 305 | # naming-style. If left empty, function names will be checked with the set 306 | # naming style. 307 | #function-rgx= 308 | 309 | # Good variable names which should always be accepted, separated by a comma. 310 | good-names=i, 311 | j, 312 | k, 313 | ex, 314 | Run, 315 | _ 316 | 317 | # Good variable names regexes, separated by a comma. If names match any regex, 318 | # they will always be accepted 319 | good-names-rgxs= 320 | 321 | # Include a hint for the correct naming format with invalid-name. 322 | include-naming-hint=no 323 | 324 | # Naming style matching correct inline iteration names. 325 | inlinevar-naming-style=any 326 | 327 | # Regular expression matching correct inline iteration names. Overrides 328 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 329 | # with the set naming style. 330 | #inlinevar-rgx= 331 | 332 | # Naming style matching correct method names. 333 | method-naming-style=snake_case 334 | 335 | # Regular expression matching correct method names. Overrides method-naming- 336 | # style. If left empty, method names will be checked with the set naming style. 337 | #method-rgx= 338 | 339 | # Naming style matching correct module names. 340 | module-naming-style=snake_case 341 | 342 | # Regular expression matching correct module names. Overrides module-naming- 343 | # style. If left empty, module names will be checked with the set naming style. 344 | #module-rgx= 345 | 346 | # Colon-delimited sets of names that determine each other's naming style when 347 | # the name regexes allow several styles. 348 | name-group= 349 | 350 | # Regular expression which should only match function or class names that do 351 | # not require a docstring. 352 | no-docstring-rgx=^_ 353 | 354 | # List of decorators that produce properties, such as abc.abstractproperty. Add 355 | # to this list to register other decorators that produce valid properties. 356 | # These decorators are taken in consideration only for invalid-name. 357 | property-classes=abc.abstractproperty 358 | 359 | # Regular expression matching correct type variable names. If left empty, type 360 | # variable names will be checked with the set naming style. 361 | #typevar-rgx= 362 | 363 | # Naming style matching correct variable names. 364 | variable-naming-style=snake_case 365 | 366 | # Regular expression matching correct variable names. Overrides variable- 367 | # naming-style. If left empty, variable names will be checked with the set 368 | # naming style. 369 | #variable-rgx= 370 | 371 | 372 | [DESIGN] 373 | 374 | # List of regular expressions of class ancestor names to ignore when counting 375 | # public methods (see R0903) 376 | exclude-too-few-public-methods= 377 | 378 | # List of qualified class names to ignore when counting class parents (see 379 | # R0901) 380 | ignored-parents= 381 | 382 | # Maximum number of arguments for function / method. 383 | max-args=5 384 | 385 | # Maximum number of attributes for a class (see R0902). 386 | max-attributes=7 387 | 388 | # Maximum number of boolean expressions in an if statement (see R0916). 389 | max-bool-expr=5 390 | 391 | # Maximum number of branch for function / method body. 392 | max-branches=12 393 | 394 | # Maximum number of locals for function / method body. 395 | max-locals=15 396 | 397 | # Maximum number of parents for a class (see R0901). 398 | max-parents=7 399 | 400 | # Maximum number of public methods for a class (see R0904). 401 | max-public-methods=20 402 | 403 | # Maximum number of return / yield for function / method body. 404 | max-returns=6 405 | 406 | # Maximum number of statements in function / method body. 407 | max-statements=50 408 | 409 | # Minimum number of public methods for a class (see R0903). 410 | min-public-methods=2 411 | 412 | 413 | [LOGGING] 414 | 415 | # The type of string formatting that logging methods do. `old` means using % 416 | # formatting, `new` is for `{}` formatting. 417 | logging-format-style=old 418 | 419 | # Logging modules to check that the string format arguments are in logging 420 | # function parameter format. 421 | logging-modules=logging 422 | 423 | 424 | [IMPORTS] 425 | 426 | # List of modules that can be imported at any level, not just the top level 427 | # one. 428 | allow-any-import-level= 429 | 430 | # Allow wildcard imports from modules that define __all__. 431 | allow-wildcard-with-all=no 432 | 433 | # Deprecated modules which should not be used, separated by a comma. 434 | deprecated-modules= 435 | 436 | # Output a graph (.gv or any supported image format) of external dependencies 437 | # to the given file (report RP0402 must not be disabled). 438 | ext-import-graph= 439 | 440 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 441 | # external) dependencies to the given file (report RP0402 must not be 442 | # disabled). 443 | import-graph= 444 | 445 | # Output a graph (.gv or any supported image format) of internal dependencies 446 | # to the given file (report RP0402 must not be disabled). 447 | int-import-graph= 448 | 449 | # Force import order to recognize a module as part of the standard 450 | # compatibility libraries. 451 | known-standard-library= 452 | 453 | # Force import order to recognize a module as part of a third party library. 454 | known-third-party=enchant 455 | 456 | # Couples of modules and preferred modules, separated by a comma. 457 | preferred-modules= 458 | 459 | 460 | [VARIABLES] 461 | 462 | # List of additional names supposed to be defined in builtins. Remember that 463 | # you should avoid defining new builtins when possible. 464 | additional-builtins= 465 | 466 | # Tells whether unused global variables should be treated as a violation. 467 | allow-global-unused-variables=yes 468 | 469 | # List of names allowed to shadow builtins 470 | allowed-redefined-builtins= 471 | 472 | # List of strings which can identify a callback function by name. A callback 473 | # name must start or end with one of those strings. 474 | callbacks=cb_, 475 | _cb 476 | 477 | # A regular expression matching the name of dummy variables (i.e. expected to 478 | # not be used). 479 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 480 | 481 | # Argument names that match this expression will be ignored. Default to name 482 | # with leading underscore. 483 | ignored-argument-names=_.*|^ignored_|^unused_ 484 | 485 | # Tells whether we should check for unused import in __init__ files. 486 | init-import=no 487 | 488 | # List of qualified module names which can have objects that can redefine 489 | # builtins. 490 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 491 | 492 | 493 | [TYPECHECK] 494 | 495 | # List of decorators that produce context managers, such as 496 | # contextlib.contextmanager. Add to this list to register other decorators that 497 | # produce valid context managers. 498 | contextmanager-decorators=contextlib.contextmanager 499 | 500 | # List of members which are set dynamically and missed by pylint inference 501 | # system, and so shouldn't trigger E1101 when accessed. Python regular 502 | # expressions are accepted. 503 | generated-members= 504 | 505 | # Tells whether to warn about missing members when the owner of the attribute 506 | # is inferred to be None. 507 | ignore-none=yes 508 | 509 | # This flag controls whether pylint should warn about no-member and similar 510 | # checks whenever an opaque object is returned when inferring. The inference 511 | # can return multiple potential results while evaluating a Python object, but 512 | # some branches might not be evaluated, which results in partial inference. In 513 | # that case, it might be useful to still emit no-member and other checks for 514 | # the rest of the inferred objects. 515 | ignore-on-opaque-inference=yes 516 | 517 | # List of symbolic message names to ignore for Mixin members. 518 | ignored-checks-for-mixins=no-member, 519 | not-async-context-manager, 520 | not-context-manager, 521 | attribute-defined-outside-init 522 | 523 | # List of class names for which member attributes should not be checked (useful 524 | # for classes with dynamically set attributes). This supports the use of 525 | # qualified names. 526 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 527 | 528 | # Show a hint with possible names when a member name was not found. The aspect 529 | # of finding the hint is based on edit distance. 530 | missing-member-hint=yes 531 | 532 | # The minimum edit distance a name should have in order to be considered a 533 | # similar match for a missing member name. 534 | missing-member-hint-distance=1 535 | 536 | # The total number of similar names that should be taken in consideration when 537 | # showing a hint for a missing member. 538 | missing-member-max-choices=1 539 | 540 | # Regex pattern to define which classes are considered mixins. 541 | mixin-class-rgx=.*[Mm]ixin 542 | 543 | # List of decorators that change the signature of a decorated function. 544 | signature-mutators= 545 | 546 | 547 | [REFACTORING] 548 | 549 | # Maximum number of nested blocks for function / method body 550 | max-nested-blocks=5 551 | 552 | # Complete name of functions that never returns. When checking for 553 | # inconsistent-return-statements if a never returning function is called then 554 | # it will be considered as an explicit return statement and no message will be 555 | # printed. 556 | never-returning-functions=sys.exit,argparse.parse_error 557 | 558 | 559 | [FORMAT] 560 | 561 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 562 | expected-line-ending-format= 563 | 564 | # Regexp for a line that is allowed to be longer than the limit. 565 | ignore-long-lines=^\s*(# )??$ 566 | 567 | # Number of spaces of indent required inside a hanging or continued line. 568 | indent-after-paren=4 569 | 570 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 571 | # tab). 572 | indent-string=' ' 573 | 574 | # Maximum number of characters on a single line. 575 | max-line-length=100 576 | 577 | # Maximum number of lines in a module. 578 | max-module-lines=1000 579 | 580 | # Allow the body of a class to be on the same line as the declaration if body 581 | # contains single statement. 582 | single-line-class-stmt=no 583 | 584 | # Allow the body of an if to be on the same line as the test if there is no 585 | # else. 586 | single-line-if-stmt=no 587 | 588 | 589 | [SIMILARITIES] 590 | 591 | # Comments are removed from the similarity computation 592 | ignore-comments=yes 593 | 594 | # Docstrings are removed from the similarity computation 595 | ignore-docstrings=yes 596 | 597 | # Imports are removed from the similarity computation 598 | ignore-imports=yes 599 | 600 | # Signatures are removed from the similarity computation 601 | ignore-signatures=yes 602 | 603 | # Minimum lines number of a similarity. 604 | min-similarity-lines=4 605 | -------------------------------------------------------------------------------- /graph_tutorial/graph_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sample-pythondjangoapp/ab3ea01cf89b6cfe7d0b8c73a3e06c9cc41ba216/graph_tutorial/graph_tutorial/__init__.py -------------------------------------------------------------------------------- /graph_tutorial/graph_tutorial/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for graph_tutorial project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'graph_tutorial.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /graph_tutorial/graph_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for graph_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-^n&2c=oa_)l_gpaxt&0)q+j5_z157*f&6v!l)k)&d5s1j6)c8w' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'tutorial', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'graph_tutorial.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'graph_tutorial.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 118 | 119 | STATIC_URL = 'static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | -------------------------------------------------------------------------------- /graph_tutorial/graph_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | """graph_tutorial URL Configuration 5 | 6 | The `urlpatterns` list routes URLs to views. For more information please see: 7 | https://docs.djangoproject.com/en/4.1/topics/http/urls/ 8 | Examples: 9 | Function views 10 | 1. Add an import: from my_app import views 11 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 12 | Class-based views 13 | 1. Add an import: from other_app.views import Home 14 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 15 | Including another URLconf 16 | 1. Import the include() function: from django.urls import include, path 17 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 18 | """ 19 | from django.contrib import admin 20 | from django.urls import path, include 21 | 22 | urlpatterns = [ 23 | path('', include('tutorial.urls')), 24 | path('admin/', admin.site.urls), 25 | ] 26 | -------------------------------------------------------------------------------- /graph_tutorial/graph_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for graph_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'graph_tutorial.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /graph_tutorial/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'graph_tutorial.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /graph_tutorial/oauth_settings.example.yml: -------------------------------------------------------------------------------- 1 | app_id: "YOUR_APP_ID_HERE" 2 | app_secret: "YOUR_APP_SECRET_HERE" 3 | redirect: "http://localhost:8000/callback" 4 | scopes: 5 | - user.read 6 | - mailboxsettings.read 7 | - calendars.readwrite 8 | authority: "https://login.microsoftonline.com/common" 9 | -------------------------------------------------------------------------------- /graph_tutorial/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==5.2.1;python_version>"3.9" 2 | Django==4.2.20;python_version<="3.9" 3 | msal==1.32.3 4 | python-dateutil==2.9.0.post0 5 | PyYAML==6.0.2 6 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sample-pythondjangoapp/ab3ea01cf89b6cfe7d0b8c73a3e06c9cc41ba216/graph_tutorial/tutorial/__init__.py -------------------------------------------------------------------------------- /graph_tutorial/tutorial/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TutorialConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'tutorial' 7 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/auth_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import yaml 5 | import msal 6 | 7 | # Load the oauth_settings.yml file 8 | stream = open('oauth_settings.yml', 'r', encoding='utf8') 9 | settings = yaml.load(stream, yaml.SafeLoader) 10 | 11 | def load_cache(request): 12 | # Check for a token cache in the session 13 | cache = msal.SerializableTokenCache() 14 | if request.session.get('token_cache'): 15 | cache.deserialize(request.session['token_cache']) 16 | 17 | return cache 18 | 19 | def save_cache(request, cache): 20 | # If cache has changed, persist back to session 21 | if cache.has_state_changed: 22 | request.session['token_cache'] = cache.serialize() 23 | 24 | def get_msal_app(cache=None): 25 | # Initialize the MSAL confidential client 26 | auth_app = msal.ConfidentialClientApplication( 27 | settings['app_id'], 28 | authority=settings['authority'], 29 | client_credential=settings['app_secret'], 30 | token_cache=cache) 31 | 32 | return auth_app 33 | 34 | # Method to generate a sign-in flow 35 | def get_sign_in_flow(): 36 | auth_app = get_msal_app() 37 | 38 | return auth_app.initiate_auth_code_flow( 39 | settings['scopes'], 40 | redirect_uri=settings['redirect']) 41 | 42 | # Method to exchange auth code for access token 43 | def get_token_from_code(request): 44 | cache = load_cache(request) 45 | auth_app = get_msal_app(cache) 46 | 47 | # Get the flow saved in session 48 | flow = request.session.pop('auth_flow', {}) 49 | 50 | result = auth_app.acquire_token_by_auth_code_flow(flow, request.GET) 51 | save_cache(request, cache) 52 | 53 | return result 54 | 55 | def store_user(request, user): 56 | time_zone = 'UTC' 57 | if 'timeZone' in user['mailboxSettings']: 58 | time_zone = user['mailboxSettings']['timeZone'] 59 | 60 | request.session['user'] = { 61 | 'is_authenticated': True, 62 | 'name': user['displayName'], 63 | 'email': user['mail'] if (user['mail'] is not None) else user['userPrincipalName'], 64 | 'timeZone': time_zone 65 | } 66 | 67 | def get_token(request): 68 | cache = load_cache(request) 69 | auth_app = get_msal_app(cache) 70 | 71 | accounts = auth_app.get_accounts() 72 | if accounts: 73 | result = auth_app.acquire_token_silent( 74 | settings['scopes'], 75 | account=accounts[0]) 76 | 77 | save_cache(request, cache) 78 | 79 | return result['access_token'] if result is not None else None 80 | 81 | def remove_user_and_token(request): 82 | if 'token_cache' in request.session: 83 | del request.session['token_cache'] 84 | 85 | if 'user' in request.session: 86 | del request.session['user'] 87 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/graph_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import json 5 | import requests 6 | 7 | GRAPH_URL = 'https://graph.microsoft.com/v1.0' 8 | 9 | def get_user(token): 10 | # Send GET to /me 11 | user = requests.get( 12 | f'{GRAPH_URL}/me', 13 | headers={ 14 | 'Authorization': f'Bearer {token}' 15 | }, 16 | params={ 17 | '$select': 'displayName,mail,mailboxSettings,userPrincipalName' 18 | }) 19 | # Return the JSON result 20 | return user.json() 21 | 22 | def get_calendar_events(token, start, end, timezone): 23 | # Set headers 24 | headers = { 25 | 'Authorization': f'Bearer {token}', 26 | 'Prefer': f'outlook.timezone="{timezone}"' 27 | } 28 | 29 | # Configure query parameters to 30 | # modify the results 31 | query_params = { 32 | 'startDateTime': start, 33 | 'endDateTime': end, 34 | '$select': 'subject,organizer,start,end', 35 | '$orderby': 'start/dateTime', 36 | '$top': '50' 37 | } 38 | 39 | # Send GET to /me/events 40 | events = requests.get(f'{GRAPH_URL}/me/calendarview', 41 | headers=headers, 42 | params=query_params) 43 | 44 | # Return the JSON result 45 | return events.json() 46 | 47 | def create_event(token, subject, start, end, attendees=None, body=None, timezone='UTC'): 48 | # Create an event object 49 | # https://docs.microsoft.com/graph/api/resources/event?view=graph-rest-1.0 50 | new_event = { 51 | 'subject': subject, 52 | 'start': { 53 | 'dateTime': start, 54 | 'timeZone': timezone 55 | }, 56 | 'end': { 57 | 'dateTime': end, 58 | 'timeZone': timezone 59 | } 60 | } 61 | 62 | if attendees: 63 | attendee_list = [] 64 | for email in attendees: 65 | # Create an attendee object 66 | # https://docs.microsoft.com/graph/api/resources/attendee?view=graph-rest-1.0 67 | attendee_list.append({ 68 | 'type': 'required', 69 | 'emailAddress': { 'address': email } 70 | }) 71 | 72 | new_event['attendees'] = attendee_list 73 | 74 | if body: 75 | # Create an itemBody object 76 | # https://docs.microsoft.com/graph/api/resources/itembody?view=graph-rest-1.0 77 | new_event['body'] = { 78 | 'contentType': 'text', 79 | 'content': body 80 | } 81 | 82 | # Set headers 83 | headers = { 84 | 'Authorization': f'Bearer {token}', 85 | 'Content-Type': 'application/json' 86 | } 87 | 88 | requests.post(f'{GRAPH_URL}/me/events', 89 | headers=headers, 90 | data=json.dumps(new_event)) 91 | 92 | #/* spell-checker: disable */ 93 | # Basic lookup for mapping Windows time zone identifiers to 94 | # IANA identifiers 95 | # Mappings taken from 96 | # https://github.com/unicode-org/cldr/blob/master/common/supplemental/windowsZones.xml 97 | zone_mappings = { 98 | 'Dateline Standard Time': 'Etc/GMT+12', 99 | 'UTC-11': 'Etc/GMT+11', 100 | 'Aleutian Standard Time': 'America/Adak', 101 | 'Hawaiian Standard Time': 'Pacific/Honolulu', 102 | 'Marquesas Standard Time': 'Pacific/Marquesas', 103 | 'Alaskan Standard Time': 'America/Anchorage', 104 | 'UTC-09': 'Etc/GMT+9', 105 | 'Pacific Standard Time (Mexico)': 'America/Tijuana', 106 | 'UTC-08': 'Etc/GMT+8', 107 | 'Pacific Standard Time': 'America/Los_Angeles', 108 | 'US Mountain Standard Time': 'America/Phoenix', 109 | 'Mountain Standard Time (Mexico)': 'America/Chihuahua', 110 | 'Mountain Standard Time': 'America/Denver', 111 | 'Central America Standard Time': 'America/Guatemala', 112 | 'Central Standard Time': 'America/Chicago', 113 | 'Easter Island Standard Time': 'Pacific/Easter', 114 | 'Central Standard Time (Mexico)': 'America/Mexico_City', 115 | 'Canada Central Standard Time': 'America/Regina', 116 | 'SA Pacific Standard Time': 'America/Bogota', 117 | 'Eastern Standard Time (Mexico)': 'America/Cancun', 118 | 'Eastern Standard Time': 'America/New_York', 119 | 'Haiti Standard Time': 'America/Port-au-Prince', 120 | 'Cuba Standard Time': 'America/Havana', 121 | 'US Eastern Standard Time': 'America/Indianapolis', 122 | 'Turks And Caicos Standard Time': 'America/Grand_Turk', 123 | 'Paraguay Standard Time': 'America/Asuncion', 124 | 'Atlantic Standard Time': 'America/Halifax', 125 | 'Venezuela Standard Time': 'America/Caracas', 126 | 'Central Brazilian Standard Time': 'America/Cuiaba', 127 | 'SA Western Standard Time': 'America/La_Paz', 128 | 'Pacific SA Standard Time': 'America/Santiago', 129 | 'Newfoundland Standard Time': 'America/St_Johns', 130 | 'Tocantins Standard Time': 'America/Araguaina', 131 | 'E. South America Standard Time': 'America/Sao_Paulo', 132 | 'SA Eastern Standard Time': 'America/Cayenne', 133 | 'Argentina Standard Time': 'America/Buenos_Aires', 134 | 'Greenland Standard Time': 'America/Godthab', 135 | 'Montevideo Standard Time': 'America/Montevideo', 136 | 'Magallanes Standard Time': 'America/Punta_Arenas', 137 | 'Saint Pierre Standard Time': 'America/Miquelon', 138 | 'Bahia Standard Time': 'America/Bahia', 139 | 'UTC-02': 'Etc/GMT+2', 140 | 'Azores Standard Time': 'Atlantic/Azores', 141 | 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', 142 | 'UTC': 'Etc/GMT', 143 | 'GMT Standard Time': 'Europe/London', 144 | 'Greenwich Standard Time': 'Atlantic/Reykjavik', 145 | 'Sao Tome Standard Time': 'Africa/Sao_Tome', 146 | 'Morocco Standard Time': 'Africa/Casablanca', 147 | 'W. Europe Standard Time': 'Europe/Berlin', 148 | 'Central Europe Standard Time': 'Europe/Budapest', 149 | 'Romance Standard Time': 'Europe/Paris', 150 | 'Central European Standard Time': 'Europe/Warsaw', 151 | 'W. Central Africa Standard Time': 'Africa/Lagos', 152 | 'Jordan Standard Time': 'Asia/Amman', 153 | 'GTB Standard Time': 'Europe/Bucharest', 154 | 'Middle East Standard Time': 'Asia/Beirut', 155 | 'Egypt Standard Time': 'Africa/Cairo', 156 | 'E. Europe Standard Time': 'Europe/Chisinau', 157 | 'Syria Standard Time': 'Asia/Damascus', 158 | 'West Bank Standard Time': 'Asia/Hebron', 159 | 'South Africa Standard Time': 'Africa/Johannesburg', 160 | 'FLE Standard Time': 'Europe/Kiev', 161 | 'Israel Standard Time': 'Asia/Jerusalem', 162 | 'Kaliningrad Standard Time': 'Europe/Kaliningrad', 163 | 'Sudan Standard Time': 'Africa/Khartoum', 164 | 'Libya Standard Time': 'Africa/Tripoli', 165 | 'Namibia Standard Time': 'Africa/Windhoek', 166 | 'Arabic Standard Time': 'Asia/Baghdad', 167 | 'Turkey Standard Time': 'Europe/Istanbul', 168 | 'Arab Standard Time': 'Asia/Riyadh', 169 | 'Belarus Standard Time': 'Europe/Minsk', 170 | 'Russian Standard Time': 'Europe/Moscow', 171 | 'E. Africa Standard Time': 'Africa/Nairobi', 172 | 'Iran Standard Time': 'Asia/Tehran', 173 | 'Arabian Standard Time': 'Asia/Dubai', 174 | 'Astrakhan Standard Time': 'Europe/Astrakhan', 175 | 'Azerbaijan Standard Time': 'Asia/Baku', 176 | 'Russia Time Zone 3': 'Europe/Samara', 177 | 'Mauritius Standard Time': 'Indian/Mauritius', 178 | 'Saratov Standard Time': 'Europe/Saratov', 179 | 'Georgian Standard Time': 'Asia/Tbilisi', 180 | 'Volgograd Standard Time': 'Europe/Volgograd', 181 | 'Caucasus Standard Time': 'Asia/Yerevan', 182 | 'Afghanistan Standard Time': 'Asia/Kabul', 183 | 'West Asia Standard Time': 'Asia/Tashkent', 184 | 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', 185 | 'Pakistan Standard Time': 'Asia/Karachi', 186 | 'Qyzylorda Standard Time': 'Asia/Qyzylorda', 187 | 'India Standard Time': 'Asia/Calcutta', 188 | 'Sri Lanka Standard Time': 'Asia/Colombo', 189 | 'Nepal Standard Time': 'Asia/Katmandu', 190 | 'Central Asia Standard Time': 'Asia/Almaty', 191 | 'Bangladesh Standard Time': 'Asia/Dhaka', 192 | 'Omsk Standard Time': 'Asia/Omsk', 193 | 'Myanmar Standard Time': 'Asia/Rangoon', 194 | 'SE Asia Standard Time': 'Asia/Bangkok', 195 | 'Altai Standard Time': 'Asia/Barnaul', 196 | 'W. Mongolia Standard Time': 'Asia/Hovd', 197 | 'North Asia Standard Time': 'Asia/Krasnoyarsk', 198 | 'N. Central Asia Standard Time': 'Asia/Novosibirsk', 199 | 'Tomsk Standard Time': 'Asia/Tomsk', 200 | 'China Standard Time': 'Asia/Shanghai', 201 | 'North Asia East Standard Time': 'Asia/Irkutsk', 202 | 'Singapore Standard Time': 'Asia/Singapore', 203 | 'W. Australia Standard Time': 'Australia/Perth', 204 | 'Taipei Standard Time': 'Asia/Taipei', 205 | 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', 206 | 'Aus Central W. Standard Time': 'Australia/Eucla', 207 | 'Transbaikal Standard Time': 'Asia/Chita', 208 | 'Tokyo Standard Time': 'Asia/Tokyo', 209 | 'North Korea Standard Time': 'Asia/Pyongyang', 210 | 'Korea Standard Time': 'Asia/Seoul', 211 | 'Yakutsk Standard Time': 'Asia/Yakutsk', 212 | 'Cen. Australia Standard Time': 'Australia/Adelaide', 213 | 'AUS Central Standard Time': 'Australia/Darwin', 214 | 'E. Australia Standard Time': 'Australia/Brisbane', 215 | 'AUS Eastern Standard Time': 'Australia/Sydney', 216 | 'West Pacific Standard Time': 'Pacific/Port_Moresby', 217 | 'Tasmania Standard Time': 'Australia/Hobart', 218 | 'Vladivostok Standard Time': 'Asia/Vladivostok', 219 | 'Lord Howe Standard Time': 'Australia/Lord_Howe', 220 | 'Bougainville Standard Time': 'Pacific/Bougainville', 221 | 'Russia Time Zone 10': 'Asia/Srednekolymsk', 222 | 'Magadan Standard Time': 'Asia/Magadan', 223 | 'Norfolk Standard Time': 'Pacific/Norfolk', 224 | 'Sakhalin Standard Time': 'Asia/Sakhalin', 225 | 'Central Pacific Standard Time': 'Pacific/Guadalcanal', 226 | 'Russia Time Zone 11': 'Asia/Kamchatka', 227 | 'New Zealand Standard Time': 'Pacific/Auckland', 228 | 'UTC+12': 'Etc/GMT-12', 229 | 'Fiji Standard Time': 'Pacific/Fiji', 230 | 'Chatham Islands Standard Time': 'Pacific/Chatham', 231 | 'UTC+13': 'Etc/GMT-13', 232 | 'Tonga Standard Time': 'Pacific/Tongatapu', 233 | 'Samoa Standard Time': 'Pacific/Apia', 234 | 'Line Islands Standard Time': 'Pacific/Kiritimati' 235 | } 236 | 237 | def get_iana_from_windows(windows_tz_name): 238 | if windows_tz_name in zone_mappings: 239 | return zone_mappings[windows_tz_name] 240 | 241 | # Assume if not found value is 242 | # already an IANA name 243 | return windows_tz_name 244 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sample-pythondjangoapp/ab3ea01cf89b6cfe7d0b8c73a3e06c9cc41ba216/graph_tutorial/tutorial/migrations/__init__.py -------------------------------------------------------------------------------- /graph_tutorial/tutorial/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/static/tutorial/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 4.5rem; 3 | } 4 | 5 | .alert-pre { 6 | word-wrap: break-word; 7 | word-break: break-all; 8 | white-space: pre-wrap; 9 | } 10 | 11 | .external-link { 12 | padding-top: 6px; 13 | } 14 | 15 | .avatar-link { 16 | padding-top: 4px; 17 | padding-bottom: 4px; 18 | } 19 | -------------------------------------------------------------------------------- /graph_tutorial/tutorial/static/tutorial/no-profile-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/msgraph-sample-pythondjangoapp/ab3ea01cf89b6cfe7d0b8c73a3e06c9cc41ba216/graph_tutorial/tutorial/static/tutorial/no-profile-photo.png -------------------------------------------------------------------------------- /graph_tutorial/tutorial/templates/tutorial/calendar.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | {% extends "tutorial/layout.html" %} 5 | {% block content %} 6 |
Organizer | 12 |Subject | 13 |Start | 14 |End | 15 |
---|---|---|---|
{{ event.organizer.emailAddress.name }} | 22 |{{ event.subject }} | 23 |{{ event.start.dateTime|date:'n/d/Y g:i A' }} | 24 |{{ event.end.dateTime|date:'n/d/Y g:i A' }} | 25 |
This sample app shows how to use the Microsoft Graph API to access a user's data from Python
9 | {% if user.is_authenticated %} 10 |Use the navigation bar at the top of the page to get started.
12 | {% else %} 13 | Click here to sign in 14 | {% endif %} 15 |{{ error.message }}
75 | {% if error.debug %} 76 |{{ error.debug }}
77 | {% endif %}
78 |