├── test ├── __init__.py ├── api │ ├── __init__.py │ ├── test_search.py │ ├── test_snapshot.py │ ├── test_annotations.py │ ├── test_dashboard.py │ ├── test_team.py │ ├── test_organization.py │ ├── test_admin.py │ └── test_folder.py └── test_grafana.py ├── _config.yml ├── setup.cfg ├── grafana_api ├── __init__.py ├── api │ ├── base.py │ ├── __init__.py │ ├── search.py │ ├── admin.py │ ├── dashboard.py │ ├── folder.py │ ├── snapshots.py │ ├── datasource.py │ ├── team.py │ ├── annotations.py │ ├── user.py │ └── organization.py ├── grafana_face.py └── grafana_api.py ├── .github ├── ISSUE_TEMPLATE │ ├── missing-grafana-api-endpoint.md │ ├── incorrect-response.md │ └── bug_report.md └── main.workflow ├── Pipfile ├── PULL_REQUEST_TEMPLATE.md ├── meta.yaml ├── LICENSE ├── .gitignore ├── setup.py ├── docs └── index.html ├── README.md ├── .circleci └── config.yml └── CHANGELOG.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /grafana_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .grafana_face import GrafanaFace 2 | -------------------------------------------------------------------------------- /grafana_api/api/base.py: -------------------------------------------------------------------------------- 1 | class Base(object): 2 | def __init__(self, api): 3 | self.api = api 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/missing-grafana-api-endpoint.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Missing Grafana API endpoint 3 | about: Report missing endpoint 4 | 5 | --- 6 | 7 | **What Grafana API endpoint the library is missing?** 8 | Describe and link the Grafana API endpoint which are currently missing. 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "grafana_api" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | codecov = "~=2.0" 10 | coverage = "~=4.5" 11 | mock = {version = "*", markers = "python_version <= '2.7'"} 12 | pylint = ">=1.9" 13 | requests-mock = "~=1.6" 14 | unittest-xml-reporting = "~=2.5" 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/incorrect-response.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Incorrect response 3 | about: Create a report on incorrect API call to help us improve 4 | 5 | --- 6 | 7 | **What API call you use?** 8 | Provide a small example to reproduce the bug. 9 | 10 | **What is the actual output?** 11 | Copy or describe the actual result. 12 | 13 | **What is the expected output?** 14 | And what output is expected. 15 | -------------------------------------------------------------------------------- /grafana_api/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from .admin import Admin 3 | from .dashboard import Dashboard 4 | from .datasource import Datasource 5 | from .folder import Folder 6 | from .organization import Organization, Organizations 7 | from .search import Search 8 | from .user import User, Users 9 | from .team import Teams 10 | from .annotations import Annotations 11 | from .snapshots import Snapshots 12 | 13 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Publish" { 2 | resolves = ["publish-to-conda", "publish-to-pypi"] 3 | on = "release" 4 | } 5 | 6 | action "publish-to-conda" { 7 | uses = "m0nhawk/conda-package-publish-action@master" 8 | secrets = ["ANACONDA_USERNAME", "ANACONDA_PASSWORD"] 9 | } 10 | 11 | action "publish-to-pypi" { 12 | uses = "mariamrf/py-package-publish-action@master" 13 | secrets = ["TWINE_PASSWORD", "TWINE_USERNAME"] 14 | env = { 15 | BRANCH = "master" 16 | PYTHON_VERSION = "3.7.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. 6 | 7 | Fixes #(issue) 8 | 9 | ## Have you written tests? 10 | 11 | - [ ] Yes 12 | - [ ] No 13 | 14 | ## Checklist: 15 | 16 | - [ ] My code follows the style guidelines of this project 17 | - [ ] I have performed a self-review of my own code 18 | - [ ] I have commented my code, particularly in hard-to-understand areas 19 | - [ ] I have made corresponding changes to the documentation 20 | - [ ] My changes generate no new warnings 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Create '...' 13 | 2. Run call '...' 14 | 3. See error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Versions** 20 | - Grafana: [ ] 21 | - `grafana_api`: [ ] 22 | - Authentication: [Basic] or [Token] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set data = load_setup_py_data(setup_file='setup.py', from_recipe_dir=True) %} 2 | {% set name = data['name'] %} 3 | {% set version = data['version'] %} 4 | {% set url = data['url'] %} 5 | {% set license = data['license'] %} 6 | 7 | package: 8 | name: "{{ name|lower }}" 9 | version: "{{ version }}" 10 | 11 | source: 12 | path: . 13 | 14 | build: 15 | noarch: python 16 | number: 0 17 | script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv " 18 | 19 | requirements: 20 | host: 21 | - python 22 | - requests 23 | - pyyaml 24 | run: 25 | - python 26 | - requests 27 | - pyyaml 28 | 29 | test: 30 | imports: 31 | - grafana_api 32 | - grafana_api.api 33 | 34 | about: 35 | home: "{{ url }}" 36 | license: "{{ license }}" 37 | license_family: "{{ license }}" 38 | license_file: LICENSE 39 | summary: Yet another Python library for Grafana API 40 | doc_url: https://m0nhawk.github.io/grafana_api/ 41 | dev_url: 42 | 43 | extra: 44 | recipe-maintainers: 45 | - m0nhawk 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrew Prokhorenkov 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 | -------------------------------------------------------------------------------- /grafana_api/grafana_face.py: -------------------------------------------------------------------------------- 1 | from .grafana_api import GrafanaAPI 2 | from .api import ( 3 | Admin, 4 | Dashboard, 5 | Datasource, 6 | Folder, 7 | Organization, 8 | Organizations, 9 | Search, 10 | User, 11 | Users, 12 | Teams, 13 | Snapshots, 14 | Annotations, 15 | ) 16 | 17 | 18 | class GrafanaFace: 19 | def __init__( 20 | self, 21 | auth, 22 | host="localhost", 23 | port=None, 24 | url_path_prefix="", 25 | protocol="http", 26 | verify=True, 27 | ): 28 | self.api = GrafanaAPI( 29 | auth, 30 | host=host, 31 | port=port, 32 | url_path_prefix=url_path_prefix, 33 | protocol=protocol, 34 | verify=verify, 35 | ) 36 | self.admin = Admin(self.api) 37 | self.dashboard = Dashboard(self.api) 38 | self.datasource = Datasource(self.api) 39 | self.folder = Folder(self.api) 40 | self.organization = Organization(self.api) 41 | self.organizations = Organizations(self.api) 42 | self.search = Search(self.api) 43 | self.user = User(self.api) 44 | self.users = Users(self.api) 45 | self.teams = Teams(self.api) 46 | self.annotations = Annotations(self.api) 47 | self.snapshots = Snapshots(self.api) 48 | -------------------------------------------------------------------------------- /grafana_api/api/search.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Search(Base): 5 | def __init__(self, api): 6 | super(Search, self).__init__(api) 7 | self.api = api 8 | 9 | def search_dashboards( 10 | self, 11 | query=None, 12 | tag=None, 13 | type_=None, 14 | dashboard_ids=None, 15 | folder_ids=None, 16 | starred=None, 17 | limit=None, 18 | ): 19 | """ 20 | 21 | :param query: 22 | :param tag: 23 | :param type_: 24 | :param dashboard_ids: 25 | :param folder_ids: 26 | :param starred: 27 | :param limit: 28 | :return: 29 | """ 30 | list_dashboard_path = "/search" 31 | params = [] 32 | 33 | if query: 34 | params.append("query=%s" % query) 35 | 36 | if tag: 37 | params.append("tag=%s" % tag) 38 | 39 | if type_: 40 | params.append("type=%s" % type_) 41 | 42 | if dashboard_ids: 43 | params.append("dashboardIds=%s" % dashboard_ids) 44 | 45 | if folder_ids: 46 | params.append("folderIds=%s" % folder_ids) 47 | 48 | if starred: 49 | params.append("starred=%s" % starred) 50 | 51 | if limit: 52 | params.append("limit=%s" % limit) 53 | 54 | list_dashboard_path += "?" 55 | list_dashboard_path += "&".join(params) 56 | 57 | r = self.api.GET(list_dashboard_path) 58 | return r 59 | -------------------------------------------------------------------------------- /test/api/test_search.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | from grafana_api.grafana_api import GrafanaServerError, GrafanaClientError, GrafanaUnauthorizedError, \ 7 | GrafanaBadInputError 8 | 9 | 10 | class AnnotationsTestCase(unittest.TestCase): 11 | def setUp(self): 12 | self.cli = GrafanaFace( 13 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 14 | ) 15 | 16 | @requests_mock.Mocker() 17 | def test_search_dashboards(self, m): 18 | m.get( 19 | "http://localhost/api/search?folderIds=11&query=str_e&starred=false&tag=test_tag" 20 | "&type=dash-folder&dashboardIds=163&limit=10", 21 | json=[ 22 | { 23 | "id": 163, 24 | "uid": "000000163", 25 | "title": "Folder", 26 | "url": "/dashboards/f/000000163/folder", 27 | "type": "dash-folder", 28 | "tags": [], 29 | "isStarred": 'false', 30 | "uri": "db/folder" 31 | } 32 | ] 33 | ) 34 | 35 | result = self.cli.search.search_dashboards(query="str_e", folder_ids=11, starred="false", tag="test_tag", 36 | type_="dash-folder", dashboard_ids=163, limit=10) 37 | self.assertEqual(result[0]["id"], 163) 38 | self.assertEqual(len(result), 1) 39 | 40 | @requests_mock.Mocker() 41 | def test_search_dashboards_with_out_filter(self, m): 42 | m.get( 43 | "http://localhost/api/search", 44 | json={ 45 | "message": "Not found" 46 | }, status_code=400 47 | ) 48 | 49 | result = self.cli.search.search_dashboards() 50 | self.assertRaises(GrafanaBadInputError) 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore lock file, it's a library 2 | Pipfile.lock 3 | 4 | # Created by .ignore support plugin (hsz.mobi) 5 | ### Python template 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | .static_storage/ 61 | .media/ 62 | local_settings.py 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 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from subprocess import check_output 3 | 4 | 5 | def get_version(): 6 | try: 7 | tag = check_output( 8 | ["git", "describe", "--tags", "--abbrev=0", "--match=[0-9]*"] 9 | ) 10 | return tag.decode("utf-8").strip("\n") 11 | except Exception: 12 | raise RuntimeError( 13 | "The version number cannot be extracted from git tag in this source " 14 | "distribution; please either download the source from PyPI, or check out " 15 | "from GitHub and make sure that the git CLI is available." 16 | ) 17 | 18 | 19 | with open("README.md") as file: 20 | long_description = file.read() 21 | 22 | setup( 23 | name="grafana_api", 24 | version=get_version(), 25 | description="Yet another Python library for Grafana API", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/m0nhawk/grafana_api", 29 | author="Andrew Prokhorenkov", 30 | author_email="andrew.prokhorenkov@gmail.com", 31 | license="MIT", 32 | packages=find_packages(), 33 | install_requires=["pyyaml", "requests"], 34 | tests_require=["unittest-xml-reporting", "requests-mock"], 35 | classifiers=[ 36 | "Development Status :: 3 - Alpha", 37 | "Intended Audience :: Developers", 38 | "Topic :: Internet", 39 | "Topic :: Software Development :: Libraries", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | "Operating System :: OS Independent", 42 | "License :: OSI Approved :: MIT License", 43 | "Programming Language :: Python :: 2", 44 | "Programming Language :: Python :: 2.7", 45 | "Programming Language :: Python :: 3", 46 | "Programming Language :: Python :: 3.6", 47 | "Programming Language :: Python :: 3.7", 48 | ], 49 | zip_safe=False, 50 | ) 51 | -------------------------------------------------------------------------------- /grafana_api/api/admin.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Admin(Base): 5 | def __init__(self, api): 6 | super(Admin, self).__init__(api) 7 | self.api = api 8 | 9 | def settings(self): 10 | """ 11 | 12 | :return: 13 | """ 14 | path = "/admin/settings" 15 | r = self.api.GET(path) 16 | return r 17 | 18 | def stats(self): 19 | """ 20 | 21 | :return: 22 | """ 23 | path = "/admin/stats" 24 | r = self.api.GET(path) 25 | return r 26 | 27 | def create_user(self, user): 28 | """ 29 | 30 | :param user: 31 | :return: 32 | """ 33 | create_user_path = "/admin/users" 34 | r = self.api.POST(create_user_path, json=user) 35 | return r 36 | 37 | def change_user_password(self, user_id, password): 38 | """ 39 | 40 | :param user_id: 41 | :param password: 42 | :return: 43 | """ 44 | change_user_password_path = "/admin/users/%s/password" % user_id 45 | r = self.api.PUT(change_user_password_path, json={"password": password}) 46 | return r 47 | 48 | def change_user_permissions(self, user_id, is_grafana_admin): 49 | """ 50 | 51 | :param user_id: 52 | :param is_grafana_admin: 53 | :return: 54 | """ 55 | change_user_permissions = "/admin/users/%s/permissions" % user_id 56 | r = self.api.PUT( 57 | change_user_permissions, json={"isGrafanaAdmin": is_grafana_admin} 58 | ) 59 | return r 60 | 61 | def delete_user(self, user_id): 62 | """ 63 | 64 | :param user_id: 65 | :return: 66 | """ 67 | delete_user_path = "/admin/users/%s" % user_id 68 | r = self.api.DELETE(delete_user_path) 69 | return r 70 | 71 | def pause_all_alerts(self, pause): 72 | """ 73 | 74 | :param pause: 75 | :return: 76 | """ 77 | change_user_permissions = "/admin/pause-all-alerts" 78 | r = self.api.POST(change_user_permissions, json={"paused": pause}) 79 | return r 80 | -------------------------------------------------------------------------------- /grafana_api/api/dashboard.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Dashboard(Base): 5 | def __init__(self, api): 6 | super(Dashboard, self).__init__(api) 7 | self.api = api 8 | 9 | def get_dashboard(self, dashboard_uid): 10 | """ 11 | 12 | :param dashboard_uid: 13 | :return: 14 | """ 15 | get_dashboard_path = "/dashboards/uid/%s" % dashboard_uid 16 | r = self.api.GET(get_dashboard_path) 17 | return r 18 | 19 | def update_dashboard(self, dashboard): 20 | """ 21 | 22 | :param dashboard: 23 | :return: 24 | """ 25 | put_dashboard_path = "/dashboards/db" 26 | r = self.api.POST(put_dashboard_path, json=dashboard) 27 | return r 28 | 29 | def delete_dashboard(self, dashboard_uid): 30 | """ 31 | 32 | :param dashboard_uid: 33 | :return: 34 | """ 35 | delete_dashboard_path = "/dashboards/uid/%s" % dashboard_uid 36 | r = self.api.DELETE(delete_dashboard_path) 37 | return r 38 | 39 | def get_home_dashboard(self): 40 | """ 41 | 42 | :return: 43 | """ 44 | get_home_dashboard_path = "/dashboards/home" 45 | r = self.api.GET(get_home_dashboard_path) 46 | return r 47 | 48 | def get_dashboards_tags(self): 49 | """ 50 | 51 | :return: 52 | """ 53 | get_dashboards_tags_path = "/dashboards/tags" 54 | r = self.api.GET(get_dashboards_tags_path) 55 | return r 56 | 57 | def get_dashboard_permissions(self, dashboard_id): 58 | """ 59 | 60 | :param dashboard_id: 61 | :return: 62 | """ 63 | get_dashboard_permissions_path = "/dashboards/id/%s/permissions" % dashboard_id 64 | r = self.api.GET(get_dashboard_permissions_path) 65 | return r 66 | 67 | def update_dashboard_permissions(self, dashboard_id, items): 68 | """ 69 | 70 | :param dashboard_id: 71 | :param items: 72 | :return: 73 | """ 74 | update_dashboard_permissions_path = ( 75 | "/dashboards/id/%s/permissions" % dashboard_id 76 | ) 77 | r = self.api.POST(update_dashboard_permissions_path, json=items) 78 | return r 79 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | grafana_api 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 |
46 |
47 |

Your Project

48 | 52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 | 63 |
64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /grafana_api/api/folder.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Folder(Base): 5 | def __init__(self, api): 6 | super(Folder, self).__init__(api) 7 | self.api = api 8 | 9 | def get_all_folders(self): 10 | """ 11 | 12 | :return: 13 | """ 14 | path = "/folders" 15 | r = self.api.GET(path) 16 | return r 17 | 18 | def get_folder(self, uid): 19 | """ 20 | 21 | :param uid: 22 | :return: 23 | """ 24 | path = "/folders/%s" % uid 25 | r = self.api.GET(path) 26 | return r 27 | 28 | def create_folder(self, title, uid=None): 29 | """ 30 | 31 | :param title: 32 | :param uid: 33 | :return: 34 | """ 35 | json_data = dict(title=title) 36 | if uid is not None: 37 | json_data["uid"] = uid 38 | return self.api.POST("/folders", json=json_data) 39 | 40 | def update_folder(self, uid, title, version=None, overwrite=False): 41 | """ 42 | 43 | :param uid: 44 | :param title: 45 | :param version: 46 | :param overwrite: 47 | :return: 48 | """ 49 | body = {"title": title} 50 | if version is not None: 51 | body['version'] = version 52 | if overwrite: 53 | body['overwrite'] = True 54 | 55 | path = "/folders/%s" % uid 56 | r = self.api.PUT(path, json=body) 57 | return r 58 | 59 | def delete_folder(self, uid): 60 | """ 61 | 62 | :param uid: 63 | :return: 64 | """ 65 | path = "/folders/%s" % uid 66 | r = self.api.DELETE(path) 67 | return r 68 | 69 | def get_folder_by_id(self, folder_id): 70 | """ 71 | 72 | :param folder_id: 73 | :return: 74 | """ 75 | path = "/folders/id/%s" % folder_id 76 | r = self.api.GET(path) 77 | return r 78 | 79 | def get_folder_permissions(self,uid): 80 | """ 81 | 82 | :return: 83 | """ 84 | path = "/folders/%s/permissions" % uid 85 | r = self.api.GET(path) 86 | return r 87 | 88 | def update_folder_permissions(self, uid, items): 89 | """ 90 | 91 | :param uid: 92 | :param items: 93 | :return: 94 | """ 95 | update_folder_permissions_path = "/folders/%s/permissions" % uid 96 | r = self.api.POST(update_folder_permissions_path, json=items) 97 | return r 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grafana_api [![CircleCI](https://img.shields.io/circleci/project/github/m0nhawk/grafana_api.svg?style=flat-square&logo=circleci)](https://circleci.com/gh/m0nhawk/workflows/grafana_api/tree/master) [![GitHub license](https://img.shields.io/github/license/m0nhawk/grafana_api.svg?style=flat-square)](https://github.com/m0nhawk/grafana_api/blob/master/LICENSE) [![Codecov](https://img.shields.io/codecov/c/gh/m0nhawk/grafana_api.svg?style=flat-square)](https://codecov.io/gh/m0nhawk/grafana_api/) 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/grafana_api.svg?style=flat-square)](https://pypi.org/project/grafana-api/) [![Conda](https://img.shields.io/conda/v/m0nhawk/grafana_api.svg?style=flat-square)](https://anaconda.org/m0nhawk/grafana_api) 4 | 5 | ## What is this library for? 6 | 7 | Yet another Grafana API library for Python. Support both 2 and 3 Python versions. 8 | 9 | ## Requirements 10 | 11 | You need either 2nd or 3rd version of Python and only the `requests` library installed. 12 | 13 | ## Quick start 14 | 15 | Install the pip package: 16 | 17 | ``` 18 | pip install -U grafana_api 19 | ``` 20 | 21 | And then connect to your Grafana API endpoint: 22 | 23 | ```python 24 | from grafana_api.grafana_face import GrafanaFace 25 | 26 | grafana_api = GrafanaFace(auth='abcde....', host='api.my-grafana-host.com') 27 | 28 | # Search dashboards based on tag 29 | grafana_api.search.search_dashboards(tag='applications') 30 | 31 | # Find a user by email 32 | user = grafana_api.users.find_user('test@test.com') 33 | 34 | # Add user to team 2 35 | grafana_api.teams.add_team_member(2, user["id"]) 36 | 37 | # Create or update a dashboard 38 | grafana_api.dashboard.update_dashboard(dashboard={'dashboard': {...}, 'folderId': 0, 'overwrite': True}) 39 | 40 | # Delete a dashboard by UID 41 | grafana_api.dashboard.delete_dashboard(dashboard_uid='abcdefgh') 42 | ``` 43 | 44 | ## Status of REST API realization 45 | 46 | Work on API implementation still in progress. 47 | 48 | | API | Status | 49 | |---|---| 50 | | Admin | + | 51 | | Alerting | - | 52 | | Annotations | + | 53 | | Authentication | +- | 54 | | Dashboard | + | 55 | | Dashboard Versions | - | 56 | | Dashboard Permissions | + | 57 | | Data Source | + | 58 | | Folder | + | 59 | | Folder Permissions | + | 60 | | Folder/Dashboard Search | +- | 61 | | Organisation | + | 62 | | Other | + | 63 | | Preferences | + | 64 | | Snapshot | + | 65 | | Teams | + | 66 | | User | + | 67 | 68 | ## Issue tracker 69 | 70 | Please report any bugs and enhancement ideas using the `grafana_api` issue tracker: 71 | 72 | https://github.com/m0nhawk/grafana_api/issues 73 | 74 | Feel free to also ask questions on the tracker. 75 | 76 | ## License 77 | 78 | `grafana_api` is licensed under the terms of the MIT License (see the file 79 | [LICENSE](LICENSE)). 80 | -------------------------------------------------------------------------------- /grafana_api/api/snapshots.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Snapshots(Base): 5 | def __init__(self, api): 6 | super(Snapshots, self).__init__(api) 7 | self.api = api 8 | 9 | def create_new_snapshot( 10 | self, 11 | dashboard=None, 12 | name=None, 13 | expires=None, 14 | external=None, 15 | key=None, 16 | delete_key=None, 17 | ): 18 | """ 19 | 20 | :param dashboard: Required. The complete dashboard model. 21 | :param name: Optional. snapshot name 22 | :param expires: Optional. When the snapshot should expire in seconds. 3600 is 1 hour, 86400 is 1 day. Default is never to expire. 23 | :param external: Optional. Save the snapshot on an external server rather than locally. Default is false. 24 | :param key: Optional. Define the unique key. Required if external is true. 25 | :param deleteKey: Optional. Unique key used to delete the snapshot. It is different from the key so that only the creator can delete the snapshot. Required if external is true. 26 | :return: 27 | """ 28 | 29 | path = "/snapshots" 30 | post_json = { 31 | "dashboard": dashboard 32 | } 33 | if name: 34 | post_json["name"] = name 35 | if expires: 36 | post_json["expires"] = expires 37 | if external: 38 | post_json["external"] = external 39 | if key: 40 | post_json["key"] = key 41 | if delete_key: 42 | post_json["deleteKey"] = delete_key 43 | 44 | r = self.api.POST(path, json=post_json) 45 | return r 46 | 47 | def get_dashboard_snapshots(self): 48 | """ 49 | 50 | :return: 51 | """ 52 | path = "/dashboard/snapshots" 53 | r = self.api.GET(path) 54 | return r 55 | 56 | def get_snapshot_by_key(self, key): 57 | """ 58 | 59 | :param key: 60 | :return: 61 | """ 62 | path = "/snapshots/%s" % key 63 | r = self.api.GET(path) 64 | return r 65 | 66 | def delete_snapshot_by_key( 67 | self, 68 | snapshot_id=None 69 | ): 70 | """ 71 | 72 | :param snapshot_id: 73 | :return: 74 | """ 75 | path = "/snapshots/{}".format(snapshot_id) 76 | r = self.api.DELETE(path) 77 | 78 | return r 79 | 80 | def delete_snapshot_by_delete_key( 81 | self, 82 | snapshot_delete_key=None 83 | ): 84 | """ 85 | 86 | :param snapshot_delete_key: 87 | :return: 88 | """ 89 | path = "/snapshots-delete/{}".format(snapshot_delete_key) 90 | r = self.api.DELETE(path) 91 | 92 | return r 93 | -------------------------------------------------------------------------------- /grafana_api/api/datasource.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Datasource(Base): 5 | def __init__(self, api): 6 | super(Datasource, self).__init__(api) 7 | self.api = api 8 | 9 | def find_datasource(self, datasource_name): 10 | """ 11 | 12 | :param datasource_name: 13 | :return: 14 | """ 15 | get_datasource_path = "/datasources/name/%s" % datasource_name 16 | r = self.api.GET(get_datasource_path) 17 | return r 18 | 19 | def get_datasource_by_id(self, datasource_id): 20 | """ 21 | 22 | :param datasource_id: 23 | :return: 24 | """ 25 | get_datasource_path = "/datasources/%s" % datasource_id 26 | r = self.api.GET(get_datasource_path) 27 | return r 28 | 29 | def get_datasource_by_name(self, datasource_name): 30 | """ 31 | 32 | :param datasource_name: 33 | :return: 34 | """ 35 | get_datasource_path = "/datasources/name/%s" % datasource_name 36 | r = self.api.GET(get_datasource_path) 37 | return r 38 | 39 | def get_datasource_id_by_name(self, datasource_name): 40 | """ 41 | 42 | :param datasource_name: 43 | :return: 44 | """ 45 | get_datasource_path = "/datasources/id/%s" % datasource_name 46 | r = self.api.GET(get_datasource_path) 47 | return r 48 | 49 | def create_datasource(self, datasource): 50 | """ 51 | 52 | :param datasource: 53 | :return: 54 | """ 55 | create_datasources_path = "/datasources" 56 | r = self.api.POST(create_datasources_path, json=datasource) 57 | return r 58 | 59 | def update_datasource(self, datasource_id, datasource): 60 | """ 61 | 62 | :param datasource_id: 63 | :param datasource: 64 | :return: 65 | """ 66 | update_datasource = "/datasources/%s" % datasource_id 67 | r = self.api.PUT(update_datasource, json=datasource) 68 | return r 69 | 70 | def list_datasources(self): 71 | """ 72 | 73 | :return: 74 | """ 75 | list_datasources_path = "/datasources" 76 | r = self.api.GET(list_datasources_path) 77 | return r 78 | 79 | def delete_datasource_by_id(self, datasource_id): 80 | """ 81 | 82 | :param datasource_id: 83 | :return: 84 | """ 85 | delete_datasource = "/datasources/%s" % datasource_id 86 | r = self.api.DELETE(delete_datasource) 87 | return r 88 | 89 | def delete_datasource_by_name(self, datasource_name): 90 | """ 91 | 92 | :param datasource_name: 93 | :return: 94 | """ 95 | delete_datasource = "/datasources/name/%s" % datasource_name 96 | r = self.api.DELETE(delete_datasource) 97 | return r 98 | -------------------------------------------------------------------------------- /test/test_grafana.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | 4 | if sys.version_info > (3, 0): 5 | from unittest.mock import patch, Mock 6 | else: 7 | from mock import patch, Mock 8 | 9 | import requests 10 | 11 | from grafana_api.grafana_face import GrafanaFace 12 | from grafana_api.grafana_api import TokenAuth 13 | 14 | 15 | class MockResponse: 16 | def __init__(self, json_data, status_code): 17 | self.json_data = json_data 18 | self.status_code = status_code 19 | 20 | def json(self): 21 | return self.json_data 22 | 23 | 24 | class TestGrafanaAPI(unittest.TestCase): 25 | @patch("grafana_api.grafana_api.GrafanaAPI.__getattr__") 26 | def test_grafana_api(self, mock_get): 27 | mock_get.return_value = Mock() 28 | mock_get.return_value.return_value = """{ 29 | "email": "user@mygraf.com", 30 | "name": "admin", 31 | "login": "admin", 32 | "theme": "light", 33 | "orgId": 1, 34 | "isGrafanaAdmin": true 35 | }""" 36 | cli = GrafanaFace( 37 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="https" 38 | ) 39 | cli.users.find_user("test@test.com") 40 | 41 | def test_grafana_api_no_verify(self): 42 | cli = GrafanaFace( 43 | ("admin", "admin"), 44 | host="localhost", 45 | url_path_prefix="", 46 | protocol="https", 47 | verify=False, 48 | ) 49 | cli.api.s.get = Mock(name="get") 50 | cli.api.s.get.return_value = MockResponse( 51 | { 52 | "email": "user@mygraf.com", 53 | "name": "admin", 54 | "login": "admin", 55 | "theme": "light", 56 | "orgId": 1, 57 | "isGrafanaAdmin": True, 58 | }, 59 | 200, 60 | ) 61 | 62 | basic_auth = requests.auth.HTTPBasicAuth("admin", "admin") 63 | cli.users.find_user("test@test.com") 64 | cli.api.s.get.assert_called_once_with( 65 | "https://localhost/api/users/lookup?loginOrEmail=test@test.com", 66 | auth=basic_auth, 67 | headers=None, 68 | json=None, 69 | verify=False, 70 | ) 71 | 72 | def test_grafana_api_basic_auth(self): 73 | cli = GrafanaFace( 74 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="https",port="3000" 75 | ) 76 | self.assertTrue(isinstance(cli.api.auth, requests.auth.HTTPBasicAuth)) 77 | 78 | def test_grafana_api_token_auth(self): 79 | cli = GrafanaFace( 80 | "alongtoken012345etc", 81 | host="localhost", 82 | url_path_prefix="", 83 | protocol="https", 84 | ) 85 | self.assertTrue(isinstance(cli.api.auth, TokenAuth)) 86 | 87 | 88 | if __name__ == "__main__": 89 | import xmlrunner 90 | 91 | unittest.main(testRunner=xmlrunner.XMLTestRunner(output="test-reports")) 92 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | tests-3.7: &test-template 5 | working_directory: ~/grafana_api 6 | docker: 7 | - image: circleci/python:3.7 8 | environment: 9 | PIPENV_VENV_IN_PROJECT: true 10 | steps: 11 | - checkout # checkout source code to working directory 12 | - restore_cache: # ensure this step occurs *before* installing dependencies 13 | key: py-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "Pipfile" }} 14 | - run: 15 | command: | 16 | sudo pip install pipenv 17 | pipenv install 18 | pipenv install --dev 19 | - run: pipenv run coverage run --source grafana_api -m xmlrunner discover -o test-reports 20 | - run: pipenv run codecov 21 | - save_cache: 22 | key: py-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "Pipfile" }} 23 | paths: 24 | - ".venv" 25 | - store_test_results: 26 | path: test-reports 27 | - store_artifacts: 28 | path: test-reports 29 | 30 | tests-3.6: 31 | <<: *test-template 32 | docker: 33 | - image: circleci/python:3.6 34 | environment: 35 | PIPENV_VENV_IN_PROJECT: true 36 | 37 | tests-2.7: 38 | <<: *test-template 39 | docker: 40 | - image: circleci/python:2.7 41 | environment: 42 | PIPENV_VENV_IN_PROJECT: true 43 | 44 | build: 45 | docker: 46 | - image: circleci/python:3.7 47 | steps: 48 | - checkout 49 | - run: 50 | name: "Build package" 51 | command: | 52 | mkdir -p ./dist/ 53 | export VERSION=$(git describe --abbrev=0 --tags) 54 | echo ${VERSION} > ./dist/VERSION 55 | export PREV_VERSION=$(git describe --tags --abbrev=0 ${VERSION}^) 56 | sed '/"'${VERSION}'"/,/"'${PREV_VERSION}'"/!d;/"'${PREV_VERSION}'"/q' CHANGELOG.md | tail -n+2 | head -n-3 > ./dist/CHANGELOG 57 | python setup.py sdist 58 | python setup.py bdist_wheel 59 | - persist_to_workspace: 60 | root: . 61 | paths: 62 | - dist 63 | 64 | publish-github-release: 65 | docker: 66 | - image: cibuilds/github:0.12 67 | steps: 68 | - attach_workspace: 69 | at: ./dist 70 | - run: 71 | name: "Publish Release on GitHub" 72 | command: | 73 | VERSION=$(<./dist/dist/VERSION) 74 | rm ./dist/dist/VERSION 75 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -b "$(<./dist/dist/CHANGELOG)" -delete ${VERSION} ./dist/dist/ 76 | 77 | workflows: 78 | version: 2 79 | main: 80 | jobs: 81 | - tests-3.7 82 | - tests-3.6 83 | - tests-2.7 84 | - build: 85 | filters: 86 | tags: 87 | only: /^\d+\.\d+\.\d+$/ 88 | - publish-github-release: 89 | requires: 90 | - build 91 | filters: 92 | branches: 93 | ignore: /.*/ 94 | tags: 95 | only: /^\d+\.\d+\.\d+$/ 96 | -------------------------------------------------------------------------------- /grafana_api/grafana_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import requests.auth 3 | 4 | 5 | class GrafanaException(Exception): 6 | pass 7 | 8 | 9 | class GrafanaServerError(Exception): 10 | """ 11 | 5xx 12 | """ 13 | 14 | pass 15 | 16 | 17 | class GrafanaClientError(Exception): 18 | """ 19 | Invalid input (4xx errors) 20 | """ 21 | 22 | pass 23 | 24 | 25 | class GrafanaBadInputError(GrafanaClientError): 26 | """ 27 | 400 28 | """ 29 | 30 | pass 31 | 32 | 33 | class GrafanaUnauthorizedError(GrafanaClientError): 34 | """ 35 | 401 36 | """ 37 | 38 | pass 39 | 40 | 41 | class TokenAuth(requests.auth.AuthBase): 42 | def __init__(self, token): 43 | self.token = token 44 | 45 | def __call__(self, request): 46 | request.headers.update({"Authorization": "Bearer {0}".format(self.token)}) 47 | return request 48 | 49 | 50 | class GrafanaAPI: 51 | def __init__( 52 | self, 53 | auth, 54 | host="localhost", 55 | port=None, 56 | url_path_prefix="", 57 | protocol="http", 58 | verify=True, 59 | ): 60 | self.auth = auth 61 | self.verify = verify 62 | self.url_host = host 63 | self.url_port = port 64 | self.url_path_prefix = url_path_prefix 65 | self.url_protocol = protocol 66 | 67 | def construct_api_url(): 68 | params = { 69 | "protocol": self.url_protocol, 70 | "host": self.url_host, 71 | "url_path_prefix": self.url_path_prefix, 72 | } 73 | 74 | if self.url_port is None: 75 | url_pattern = "{protocol}://{host}/{url_path_prefix}api" 76 | else: 77 | params["port"] = self.url_port 78 | url_pattern = "{protocol}://{host}:{port}/{url_path_prefix}api" 79 | 80 | return url_pattern.format(**params) 81 | 82 | self.url = construct_api_url() 83 | 84 | self.s = requests.Session() 85 | if not isinstance(self.auth, tuple): 86 | self.auth = TokenAuth(self.auth) 87 | else: 88 | self.auth = requests.auth.HTTPBasicAuth(*self.auth) 89 | 90 | def __getattr__(self, item): 91 | def __request_runnner(url, json=None, headers=None): 92 | __url = "%s%s" % (self.url, url) 93 | runner = getattr(self.s, item.lower()) 94 | r = runner( 95 | __url, json=json, headers=headers, auth=self.auth, verify=self.verify 96 | ) 97 | try: 98 | 99 | if 500 <= r.status_code < 600: 100 | raise GrafanaServerError( 101 | "Client Error {0}: {1}".format(r.status_code, r.json()['message']) 102 | ) 103 | elif r.status_code == 400: 104 | raise GrafanaBadInputError("Bad Input: `{0}`".format(r.text)) 105 | elif r.status_code == 401: 106 | raise GrafanaUnauthorizedError("Unauthorized") 107 | elif 400 <= r.status_code < 500: 108 | raise GrafanaClientError( 109 | "Client Error {0}: {1}".format(r.status_code, r.text) 110 | ) 111 | return r.json() 112 | 113 | except Exception as error: 114 | print('Caught this error: ' + repr(error)) 115 | 116 | 117 | 118 | return __request_runnner 119 | -------------------------------------------------------------------------------- /grafana_api/api/team.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Teams(Base): 5 | def __init__(self, api): 6 | super(Teams, self).__init__(api) 7 | self.api = api 8 | 9 | def search_teams(self, query=None, page=None, perpage=None): 10 | """ 11 | 12 | :return: 13 | """ 14 | list_of_teams = [] 15 | search_teams_path = "/teams/search" 16 | params = [] 17 | 18 | if query: 19 | params.append("query=%s" % query) 20 | 21 | if page: 22 | iterate = False 23 | params.append("page=%s" % page) 24 | else: 25 | iterate = True 26 | params.append("page=%s") 27 | page = 1 28 | 29 | if perpage: 30 | params.append("perpage=%s" % perpage) 31 | 32 | search_teams_path += "?" 33 | search_teams_path += "&".join(params) 34 | 35 | if iterate: 36 | while True: 37 | teams_on_page = self.api.GET(search_teams_path % page) 38 | list_of_teams += teams_on_page["teams"] 39 | if len(list_of_teams) == teams_on_page["totalCount"]: 40 | break 41 | page += 1 42 | else: 43 | teams_on_page = self.api.GET(search_teams_path) 44 | list_of_teams += teams_on_page["teams"] 45 | 46 | return list_of_teams 47 | 48 | def get_team_by_name(self, team_name): 49 | """ 50 | 51 | :param team_name: 52 | :return: 53 | """ 54 | search_teams_path = "/teams/search" 55 | 56 | search_teams_path += "?name=%s" % team_name 57 | 58 | teams_on_page = self.api.GET(search_teams_path) 59 | return teams_on_page["teams"] 60 | 61 | def get_team(self, team_id): 62 | """ 63 | 64 | :param team_id: 65 | :return: 66 | """ 67 | get_team_path = "/teams/%s" % team_id 68 | r = self.api.GET(get_team_path) 69 | return r 70 | 71 | def add_team(self, team): 72 | """ 73 | 74 | :param team: 75 | :return: 76 | """ 77 | add_team_path = "/teams" 78 | r = self.api.POST(add_team_path, json=team) 79 | return r 80 | 81 | def update_team(self, team_id, team): 82 | """ 83 | 84 | :param team_id: 85 | :param team: 86 | :return: 87 | """ 88 | update_team_path = "/teams/%s" % team_id 89 | r = self.api.PUT(update_team_path, json=team) 90 | return r 91 | 92 | def delete_team(self, team_id): 93 | """ 94 | 95 | :param team_id: 96 | :return: 97 | """ 98 | delete_team_path = "/teams/%s" % team_id 99 | r = self.api.DELETE(delete_team_path) 100 | return True 101 | 102 | def get_team_members(self, team_id): 103 | """ 104 | 105 | :param team_id: 106 | :return: 107 | """ 108 | get_team_members_path = "/teams/%s/members" % team_id 109 | r = self.api.GET(get_team_members_path) 110 | return r 111 | 112 | def add_team_member(self, team_id, user_id): 113 | """ 114 | 115 | :param team_id: 116 | :param user_id: 117 | :return: 118 | """ 119 | add_team_member_path = "/teams/%s/members" % team_id 120 | payload = {"userId": user_id} 121 | r = self.api.POST(add_team_member_path, json=payload) 122 | return r 123 | 124 | def remove_team_member(self, team_id, user_id): 125 | """ 126 | 127 | :param team_id: 128 | :param user_id: 129 | :return: 130 | """ 131 | remove_team_member_path = "/teams/%s/members/%s" % (team_id, user_id) 132 | r = self.api.DELETE(remove_team_member_path) 133 | return r 134 | 135 | def get_team_preferences(self, team_id): 136 | """ 137 | 138 | :param team_id: 139 | :return: 140 | """ 141 | get_team_preferences_path = "/teams/%s/preferences" % team_id 142 | r = self.api.GET(get_team_preferences_path) 143 | return r 144 | 145 | def update_team_preferences(self, team_id, preferences): 146 | """ 147 | 148 | :param team_id: 149 | :param preferences: 150 | :return: 151 | """ 152 | update_team_preferences_path = "/teams/%s/preferences" % team_id 153 | r = self.api.PUT(update_team_preferences_path, json=preferences) 154 | return r 155 | -------------------------------------------------------------------------------- /grafana_api/api/annotations.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Annotations(Base): 5 | def __init__(self, api): 6 | super(Annotations, self).__init__(api) 7 | self.api = api 8 | 9 | def get_annotation( 10 | self, 11 | time_from=None, 12 | time_to=None, 13 | alert_id=None, 14 | dashboard_id=None, 15 | panel_id=None, 16 | tags=None, 17 | limit=None, 18 | ): 19 | 20 | """ 21 | :param time_from: 22 | :param time_to: 23 | :param alert_id: 24 | :param dashboard_id: 25 | :param panel_id: 26 | :param tags: 27 | :param limit: 28 | :return: 29 | """ 30 | list_annotations_path = "/annotations" 31 | params = [] 32 | 33 | if time_from: 34 | params.append("time_from=%s" % time_from) 35 | 36 | if time_to: 37 | params.append("time_to=%s" % time_to) 38 | 39 | if alert_id: 40 | params.append("alertId=%s" % alert_id) 41 | 42 | if dashboard_id: 43 | params.append("dashboardID=%s" % dashboard_id) 44 | 45 | if panel_id: 46 | params.append("panelId=%s" % panel_id) 47 | 48 | if tags: 49 | params.append("tags=%s" % tags) 50 | 51 | if limit: 52 | params.append("limit=%s" % limit) 53 | 54 | list_annotations_path += "?" 55 | list_annotations_path += "&".join(params) 56 | 57 | r = self.api.GET(list_annotations_path) 58 | 59 | return r 60 | 61 | def add_annotation( 62 | self, 63 | time_from=None, 64 | time_to=None, 65 | is_region=True, 66 | tags=None, 67 | text=None, 68 | ): 69 | 70 | """ 71 | :param time_from: 72 | :param time_to: 73 | :param is_region: 74 | :param tags: 75 | :param text: 76 | :return: 77 | """ 78 | annotations_path = "/annotations" 79 | payload = { 80 | "time": time_from, 81 | "timeEnd": time_to, 82 | "isRegion": bool(is_region), 83 | "tags": [tags], 84 | "text": text 85 | 86 | } 87 | 88 | r = self.api.POST(annotations_path, json=payload) 89 | 90 | return r 91 | 92 | def add_annotation_graphite( 93 | self, 94 | what=None, 95 | tags=True, 96 | when=None, 97 | data=None, 98 | ): 99 | """ 100 | :param what: 101 | :param tags: 102 | :param when: 103 | :param data: 104 | :return: 105 | """ 106 | 107 | annotations_path = "/annotations/graphite" 108 | payload = { 109 | "what": what, 110 | "tags": [tags], 111 | "when": when, 112 | "data": data 113 | 114 | } 115 | 116 | r = self.api.POST(annotations_path, json=payload) 117 | 118 | return r 119 | 120 | def update_annotation( 121 | self, 122 | time_from=None, 123 | time_to=None, 124 | is_region=True, 125 | tags=None, 126 | text=None, 127 | ): 128 | """ 129 | 130 | :param time_from: 131 | :param time_to: 132 | :param is_region: 133 | :param tags: 134 | :param text: 135 | :return: 136 | """ 137 | annotations_path = "/annotations" 138 | payload = { 139 | "time": time_from, 140 | "timeEnd": time_to, 141 | "isRegion": bool(is_region), 142 | "tags": [tags], 143 | "text": text 144 | 145 | } 146 | 147 | r = self.api.PUT(annotations_path, json=payload) 148 | 149 | return r 150 | 151 | def delete_annotations_by_region_id( 152 | self, 153 | region_id=None 154 | ): 155 | 156 | """ 157 | :param region_id: 158 | :return: 159 | """ 160 | annotations_path = "/annotations/region/{}".format(region_id) 161 | r = self.api.DELETE(annotations_path) 162 | 163 | return r 164 | 165 | def delete_annotations_by_id( 166 | self, 167 | annotations_id=None 168 | ): 169 | 170 | """ 171 | :param annotations_id: 172 | :return: 173 | """ 174 | annotations_path = "/annotations/{}".format(annotations_id) 175 | r = self.api.DELETE(annotations_path) 176 | 177 | return r 178 | -------------------------------------------------------------------------------- /grafana_api/api/user.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Users(Base): 5 | def __init__(self, api): 6 | super(Users, self).__init__(api) 7 | self.api = api 8 | 9 | def search_users(self, query=None, page=None, perpage=None): 10 | """ 11 | 12 | :return: 13 | """ 14 | list_of_users = [] 15 | users_on_page = None 16 | show_users_path = "/users" 17 | params = [] 18 | 19 | if query: 20 | params.append("query=%s" % query) 21 | 22 | if page: 23 | iterate = False 24 | params.append("page=%s" % page) 25 | else: 26 | iterate = True 27 | params.append("page=%s") 28 | page = 1 29 | 30 | if perpage: 31 | params.append("perpage=%s" % perpage) 32 | 33 | show_users_path += "?" 34 | show_users_path += "&".join(params) 35 | 36 | if iterate: 37 | while users_on_page is not []: 38 | users_on_page = self.api.GET(show_users_path % page) 39 | list_of_users += users_on_page 40 | page += 1 41 | else: 42 | users_on_page = self.api.GET(show_users_path) 43 | list_of_users += users_on_page 44 | 45 | return list_of_users 46 | 47 | def get_user(self, user_id): 48 | """ 49 | 50 | :param user_id: 51 | :return: 52 | """ 53 | get_user_path = "/users/%s" % user_id 54 | r = self.api.GET(get_user_path) 55 | return r 56 | 57 | def find_user(self, login_or_email): 58 | """ 59 | 60 | :param login_or_email: 61 | :return: 62 | """ 63 | search_user_path = "/users/lookup?loginOrEmail=%s" % login_or_email 64 | r = self.api.GET(search_user_path) 65 | return r 66 | 67 | def update_user(self, user_id, user): 68 | """ 69 | 70 | :param user_id: 71 | :param user: 72 | :return: 73 | """ 74 | update_user_path = "/users/%s" % user_id 75 | r = self.api.PUT(update_user_path, json=user) 76 | return r 77 | 78 | def get_user_organisations(self, user_id): 79 | """ 80 | 81 | :param user_id: 82 | :return: 83 | """ 84 | get_user_organisations_path = "/users/%s/orgs" % user_id 85 | r = self.api.GET(get_user_organisations_path) 86 | return r 87 | 88 | 89 | class User(Base): 90 | def __init__(self, api): 91 | super(User, self).__init__(api) 92 | self.api = api 93 | self.path = "/user" 94 | 95 | def get_actual_user(self): 96 | """ 97 | 98 | :return: 99 | """ 100 | get_actual_user_path = "/user" 101 | r = self.api.GET(get_actual_user_path) 102 | return r 103 | 104 | def change_actual_user_password(self, old_password, new_password): 105 | """ 106 | 107 | :param old_password: 108 | :param new_password: 109 | :return: 110 | """ 111 | change_actual_user_password_path = "/user/password" 112 | change_actual_user_password_json = { 113 | "oldPassword": old_password, 114 | "newPassword": new_password, 115 | "confirmNew": new_password, 116 | } 117 | r = self.api.PUT( 118 | change_actual_user_password_path, json=change_actual_user_password_json 119 | ) 120 | return r 121 | 122 | def switch_user_organisation(self, user_id, organisation_id): 123 | """ 124 | 125 | :param user_id: 126 | :param organisation_id: 127 | :return: 128 | """ 129 | switch_user_organisation_path = "/users/%s/using/%s" % ( 130 | user_id, 131 | organisation_id, 132 | ) 133 | r = self.api.POST(switch_user_organisation_path) 134 | return r 135 | 136 | def switch_actual_user_organisation(self, organisation_id): 137 | """ 138 | 139 | :param organisation_id: 140 | :return: 141 | """ 142 | switch_actual_user_organisation_path = "/user/using/%s" % organisation_id 143 | r = self.api.POST(switch_actual_user_organisation_path) 144 | return r 145 | 146 | def get_actual_user_organisations(self): 147 | """ 148 | 149 | :return: 150 | """ 151 | get_actual_user_organisations_path = "/user/orgs" 152 | r = self.api.GET(get_actual_user_organisations_path) 153 | return r 154 | 155 | def star_actual_user_dashboard(self, dashboard_id): 156 | """ 157 | 158 | :param dashboard_id: 159 | :return: 160 | """ 161 | star_dashboard = "/user/stars/dashboard/%s" % dashboard_id 162 | r = self.api.POST(star_dashboard) 163 | return r 164 | 165 | def unstar_actual_user_dashboard(self, dashboard_id): 166 | """ 167 | 168 | :param dashboard_id: 169 | :return: 170 | """ 171 | unstar_dashboard = "/user/stars/dashboard/%s" % dashboard_id 172 | r = self.api.DELETE(unstar_dashboard) 173 | return r 174 | -------------------------------------------------------------------------------- /test/api/test_snapshot.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | 7 | 8 | class SnapshotTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.cli = GrafanaFace( 11 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 12 | ) 13 | 14 | @requests_mock.Mocker() 15 | def test_create_new_snapshot(self, m): 16 | m.post( 17 | "http://localhost/api/snapshots", 18 | json={ 19 | "deleteKey": "XXXXXXX", 20 | "deleteUrl": "myurl/api/snapshots.py-delete/XXXXXXX", 21 | "key": "YYYYYYY", 22 | "url": "myurl/dashboard/snapshot/YYYYYYY" 23 | }, 24 | ) 25 | snapshot = self.cli.snapshots.create_new_snapshot(dashboard={ 26 | "editable": "false", 27 | "hideControls": "true", 28 | "nav": [ 29 | { 30 | "enable": "false", 31 | "type": "timepicker" 32 | } 33 | ], 34 | "rows": [ 35 | { 36 | 37 | } 38 | ], 39 | "style": "dark", 40 | "tags": [], 41 | "templating": { 42 | "list": [ 43 | ] 44 | }, 45 | "time": { 46 | }, 47 | "timezone": "browser", 48 | "title": "Home", 49 | "version": 5 50 | }, name="Test", key="YYYYYYY", delete_key="XXXXXXX", external=True, expires=3600) 51 | self.assertEqual(snapshot["key"], "YYYYYYY") 52 | 53 | @requests_mock.Mocker() 54 | def test_create_new_snapshot_without_optional(self, m): 55 | m.post( 56 | "http://localhost/api/snapshots", 57 | json={ 58 | "deleteKey": "XXXXXXX", 59 | "deleteUrl": "myurl/api/snapshots.py-delete/XXXXXXX", 60 | "key": "YYYYYYY", 61 | "url": "myurl/dashboard/snapshot/YYYYYYY" 62 | }, 63 | ) 64 | snapshot = self.cli.snapshots.create_new_snapshot(dashboard={ 65 | "editable": "false", 66 | "hideControls": "true", 67 | "nav": [ 68 | { 69 | "enable": "false", 70 | "type": "timepicker" 71 | } 72 | ], 73 | "rows": [ 74 | { 75 | 76 | } 77 | ], 78 | "style": "dark", 79 | "tags": [], 80 | "templating": { 81 | "list": [ 82 | ] 83 | }, 84 | "time": { 85 | }, 86 | "timezone": "browser", 87 | "title": "Home", 88 | "version": 5 89 | }) 90 | self.assertEqual(snapshot["key"], "YYYYYYY") 91 | 92 | @requests_mock.Mocker() 93 | def test_get_dashboard_snapshots(self, m): 94 | m.get( 95 | "http://localhost/api/dashboard/snapshots", 96 | json=[ 97 | { 98 | "id": 8, 99 | "name": "Home", 100 | "key": "YYYYYYY", 101 | "orgId": 1, 102 | "userId": 1, 103 | "external": False, 104 | "externalUrl": "", 105 | "expires": "2200-13-32T25:23:23+02:00", 106 | "created": "2200-13-32T28:24:23+02:00", 107 | "updated": "2200-13-32T28:24:23+02:00" 108 | } 109 | ] 110 | ) 111 | dashboards = self.cli.snapshots.get_dashboard_snapshots() 112 | self.assertEqual(len(dashboards), 1) 113 | 114 | @requests_mock.Mocker() 115 | def test_get_snapshot_by_key(self, m): 116 | m.get( 117 | "http://localhost/api/snapshots/YYYYYYY", 118 | json=[ 119 | { 120 | "id": 8, 121 | "name": "Home", 122 | "key": "YYYYYYY", 123 | "orgId": 1, 124 | "userId": 1, 125 | "external": False, 126 | "externalUrl": "", 127 | "expires": "2200-13-32T25:23:23+02:00", 128 | "created": "2200-13-32T28:24:23+02:00", 129 | "updated": "2200-13-32T28:24:23+02:00" 130 | } 131 | ] 132 | ) 133 | dashboards = self.cli.snapshots.get_snapshot_by_key(key="YYYYYYY") 134 | self.assertEqual(len(dashboards), 1) 135 | 136 | @requests_mock.Mocker() 137 | def test_delete_snapshot_by_key(self, m): 138 | m.delete('http://localhost/api/snapshots/YYYYYYY', json={"message": "Snapshot deleted. It might take an hour " 139 | "before it's cleared from any CDN " 140 | "caches."}) 141 | annotation = self.cli.snapshots.delete_snapshot_by_key(snapshot_id="YYYYYYY") 142 | self.assertEqual(annotation['message'], "Snapshot deleted. It might take an hour before it's cleared from any " 143 | "CDN caches.") 144 | 145 | @requests_mock.Mocker() 146 | def test_delete_snapshot_by_delete_key(self, m): 147 | m.delete('http://localhost/api/snapshots-delete/XXXXXXX', json={"message": "Snapshot deleted. It might take an hour " 148 | "before it's cleared from any CDN " 149 | "caches."}) 150 | annotation = self.cli.snapshots.delete_snapshot_by_delete_key(snapshot_delete_key="XXXXXXX") 151 | self.assertEqual(annotation['message'], "Snapshot deleted. It might take an hour before it's cleared from any " 152 | "CDN caches.") 153 | 154 | -------------------------------------------------------------------------------- /grafana_api/api/organization.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Organization(Base): 5 | def __init__(self, api): 6 | super(Organization, self).__init__(api) 7 | self.api = api 8 | 9 | def find_organization(self, org_name): 10 | """ 11 | 12 | :param org_name: 13 | :return: 14 | """ 15 | get_org_path = "/orgs/name/%s" % org_name 16 | r = self.api.GET(get_org_path) 17 | return r 18 | 19 | def get_current_organization(self): 20 | """ 21 | 22 | :return: 23 | """ 24 | get_current_organization_path = "/org" 25 | r = self.api.GET(get_current_organization_path) 26 | return r 27 | 28 | def create_organization(self, organization): 29 | """ 30 | 31 | :param organization: 32 | :return: 33 | """ 34 | create_orgs_path = "/orgs" 35 | r = self.api.POST(create_orgs_path, json={"name": organization["name"]}) 36 | return r 37 | 38 | def update_current_organization(self, organization): 39 | """ 40 | 41 | :param organization: 42 | :return: 43 | """ 44 | update_current_organization_path = "/org" 45 | r = self.api.PUT(update_current_organization_path, json=organization) 46 | return r 47 | 48 | def get_current_organization_users(self): 49 | """ 50 | 51 | :return: 52 | """ 53 | get_current_organization_users_path = "/org/users" 54 | r = self.api.GET(get_current_organization_users_path) 55 | return r 56 | 57 | def add_user_current_organization(self, user): 58 | """ 59 | 60 | :param user: 61 | :return: 62 | """ 63 | add_user_current_organization_path = "/org/users" 64 | r = self.api.POST(add_user_current_organization_path, json=user) 65 | return r 66 | 67 | def update_user_current_organization(self, user_id, user): 68 | """ 69 | 70 | :param user_id: 71 | :param user: 72 | :return: 73 | """ 74 | update_user_current_organization_path = "/org/users/%s" % user_id 75 | r = self.api.PATCH(update_user_current_organization_path, json=user) 76 | return r 77 | 78 | def delete_user_current_organization(self, user_id): 79 | """ 80 | 81 | :param user_id: 82 | :return: 83 | """ 84 | delete_user_current_organization_path = "/org/users/%s" % user_id 85 | r = self.api.DELETE(delete_user_current_organization_path) 86 | return r 87 | 88 | 89 | class Organizations(Base): 90 | def __init__(self, api): 91 | super(Organizations, self).__init__(api) 92 | self.api = api 93 | self.path = "/users" 94 | 95 | def update_organization(self, organization_id, organization): 96 | """ 97 | 98 | :param organization_id: 99 | :param organization: 100 | :return: 101 | """ 102 | update_org_path = "/orgs/%s" % organization_id 103 | r = self.api.PUT(update_org_path, json=organization) 104 | return r 105 | 106 | def delete_organization(self, organization_id): 107 | """ 108 | 109 | :param organization_id: 110 | :return: 111 | """ 112 | delete_org_path = "/orgs/%s" % organization_id 113 | r = self.api.DELETE(delete_org_path) 114 | return r 115 | 116 | def list_organization(self): 117 | """ 118 | 119 | :return: 120 | """ 121 | search_org_path = "/orgs" 122 | r = self.api.GET(search_org_path) 123 | return r 124 | 125 | def switch_organization(self, organization_id): 126 | """ 127 | 128 | :param organization_id: 129 | :return: 130 | """ 131 | switch_user_organization = "/user/using/%s" % organization_id 132 | r = self.api.POST(switch_user_organization) 133 | return r 134 | 135 | def organization_user_list(self, organization_id): 136 | """ 137 | 138 | :param organization_id: 139 | :return: 140 | """ 141 | users_in_org = "/orgs/%s/users" % organization_id 142 | r = self.api.GET(users_in_org) 143 | return r 144 | 145 | def organization_user_add(self, organization_id, user): 146 | """ 147 | 148 | :param organization_id: 149 | :param user: 150 | :return: 151 | """ 152 | add_user_path = "/orgs/%s/users" % organization_id 153 | r = self.api.POST(add_user_path, json=user) 154 | return r 155 | 156 | def organization_user_update(self, organization_id, user_id, user_role): 157 | """ 158 | 159 | :param organization_id: 160 | :param user_id: 161 | :param user_role: 162 | :return: 163 | """ 164 | patch_user = "/orgs/%s/users/%s" % (organization_id, user_id) 165 | r = self.api.PATCH(patch_user, json={"role": user_role}) 166 | return r 167 | 168 | def organization_user_delete(self, organization_id, user_id): 169 | """ 170 | 171 | :param organization_id: 172 | :param user_id: 173 | :return: 174 | """ 175 | delete_user = "/orgs/%s/users/%s" % (organization_id, user_id) 176 | r = self.api.DELETE(delete_user) 177 | return r 178 | 179 | def organization_preference_get(self): 180 | """ 181 | :return: 182 | """ 183 | update_preference = "/org/preferences" 184 | r = self.api.GET(update_preference) 185 | return r 186 | 187 | def organization_preference_update( 188 | self, theme="", home_dashboard_id=0, timezone="utc" 189 | ): 190 | """ 191 | 192 | :param theme: 193 | :param home_dashboard_id: 194 | :param timezone: 195 | :return: 196 | """ 197 | update_preference = "/org/preferences" 198 | r = self.api.PUT( 199 | update_preference, 200 | json={ 201 | "theme": theme, 202 | "homeDashboardId": home_dashboard_id, 203 | "timezone": timezone, 204 | }, 205 | ) 206 | return r 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [Unreleased] 3 | 4 | 5 | 6 | ## [0.8.1] - 2019-07-20 7 | ### Chore 8 | - **changelog:** 0.8.0 9 | - **dep:** remove Pipfile, now in .gitignore 10 | - **dep:** Python 2 compatible pylint version 11 | - **release:** release body with latest changes 12 | - **test:** run all the tests 13 | - **ver:** 0.8.1 14 | 15 | 16 | 17 | ## [0.8.0] - 2019-07-20 18 | ### Chore 19 | - **changelog:** 0.7.5 20 | - **dep:** improve dependency handling 21 | 22 | ### Feat 23 | - **circleci:** test on Python 2.7, 3.6 & 3.7 24 | - **dep:** adding missing dependency for Python 2 testing 25 | - **travis:** remove 26 | 27 | 28 | 29 | ## [0.7.5] - 2019-06-06 30 | ### Chore 31 | - **changelog:** 0.7.4 32 | 33 | 34 | 35 | ## [0.7.4] - 2019-06-05 36 | ### Chore 37 | - **changelog:** 0.7.3 38 | - **codestyle:** reformat 39 | - **dep:** update 40 | - **deps:** bump requests from 2.21.0 to 2.22.0 41 | 42 | ### Fix 43 | - **api:** python2 Teams support, fix [#24](https://github.com/m0nhawk/grafana_api/issues/24) 44 | 45 | 46 | 47 | ## [0.7.3] - 2019-05-04 48 | ### Chore 49 | - **changelog:** 0.7.2 50 | 51 | 52 | 53 | ## [0.7.2] - 2019-04-28 54 | ### Chore 55 | - **changelog:** v0.7.1 56 | 57 | 58 | 59 | ## [0.7.1] - 2019-04-22 60 | ### Chore 61 | - **dependency:** fix "urllib3" version 62 | 63 | ### Feat 64 | - **circleci:** remove debug statements 65 | 66 | ### Fix 67 | - **README:** remove coveralls badge 68 | - **circleci:** do not put VERSION file to Github release 69 | 70 | 71 | 72 | ## [0.7.0] - 2019-04-05 73 | ### Feat 74 | - **auto-deploy:** build package automatically 75 | - **circleci:** pre-create git version 76 | 77 | ### Fix 78 | - **organization:** fix [#11](https://github.com/m0nhawk/grafana_api/issues/11), rename to “Organization” the same as in Grafana API 79 | 80 | 81 | 82 | ## [0.6.0] - 2019-03-31 83 | ### Chore 84 | - **anaconda:** anaconda package configuration 85 | - **deploy:** add Python package deployment 86 | - **gitlab:** remove Gitlab CI 87 | - **setup:** more configurable Python package configuration 88 | 89 | ### Feat 90 | - **deploy:** setup Github actions for deployment 91 | - **test:** store Artifacts 92 | - **test:** JUnit reporting for CircleCI 93 | 94 | ### Test 95 | - **codecov:** update badge 96 | - **codecov:** move to Codecov 97 | - **coverage:** alternative Python library for coveralls 98 | - **coverage:** remove sonacloud 99 | - **coverage:** include only “grafana_api” folder for coverage 100 | - **coverage:** coveralls support 101 | - **gitlab:** update Gitlab CI 102 | - **test:** test file 103 | - **test:** remove test file 104 | 105 | 106 | 107 | ## [v0.5.2] - 2019-02-26 108 | 109 | 110 | ## [v0.5.1] - 2019-02-04 111 | 112 | 113 | ## [v0.5.0] - 2018-11-25 114 | 115 | 116 | ## [v0.3.5] - 2018-11-24 117 | ### Workaround 118 | - always deploy 119 | 120 | 121 | 122 | ## [v0.3.4] - 2018-11-24 123 | 124 | 125 | ## [v0.3.3] - 2018-11-24 126 | ### Feat 127 | - **tests:** improve coverage for SonarCloud 128 | 129 | 130 | 131 | ## [v0.3.1] - 2018-11-24 132 | 133 | 134 | ## [v0.3.0] - 2018-11-24 135 | 136 | 137 | ## [v0.2.9] - 2018-11-24 138 | 139 | 140 | ## [v0.2.8] - 2018-11-24 141 | 142 | 143 | ## [v0.2.6] - 2018-11-24 144 | 145 | 146 | ## [v0.2.2] - 2018-11-24 147 | 148 | 149 | ## [v0.2.1] - 2018-11-24 150 | 151 | 152 | ## [v0.2.0] - 2018-11-24 153 | 154 | 155 | ## [v0.1.7] - 2018-11-24 156 | 157 | 158 | ## [v0.1.6] - 2018-11-24 159 | 160 | 161 | ## [v0.1.5] - 2018-11-24 162 | 163 | 164 | ## [v0.1.4] - 2018-11-24 165 | 166 | 167 | ## [v0.1.3] - 2018-11-24 168 | 169 | 170 | ## v0.3.6 - 2018-11-03 171 | ### Feat 172 | - **tests:** improve coverage for SonarCloud 173 | 174 | ### Workaround 175 | - always deploy 176 | 177 | 178 | [Unreleased]: https://github.com/m0nhawk/grafana_api/compare/0.8.1...HEAD 179 | [0.8.1]: https://github.com/m0nhawk/grafana_api/compare/0.8.0...0.8.1 180 | [0.8.0]: https://github.com/m0nhawk/grafana_api/compare/0.7.5...0.8.0 181 | [0.7.5]: https://github.com/m0nhawk/grafana_api/compare/0.7.4...0.7.5 182 | [0.7.4]: https://github.com/m0nhawk/grafana_api/compare/0.7.3...0.7.4 183 | [0.7.3]: https://github.com/m0nhawk/grafana_api/compare/0.7.2...0.7.3 184 | [0.7.2]: https://github.com/m0nhawk/grafana_api/compare/0.7.1...0.7.2 185 | [0.7.1]: https://github.com/m0nhawk/grafana_api/compare/0.7.0...0.7.1 186 | [0.7.0]: https://github.com/m0nhawk/grafana_api/compare/0.6.0...0.7.0 187 | [0.6.0]: https://github.com/m0nhawk/grafana_api/compare/v0.5.2...0.6.0 188 | [v0.5.2]: https://github.com/m0nhawk/grafana_api/compare/v0.5.1...v0.5.2 189 | [v0.5.1]: https://github.com/m0nhawk/grafana_api/compare/v0.5.0...v0.5.1 190 | [v0.5.0]: https://github.com/m0nhawk/grafana_api/compare/v0.3.5...v0.5.0 191 | [v0.3.5]: https://github.com/m0nhawk/grafana_api/compare/v0.3.4...v0.3.5 192 | [v0.3.4]: https://github.com/m0nhawk/grafana_api/compare/v0.3.3...v0.3.4 193 | [v0.3.3]: https://github.com/m0nhawk/grafana_api/compare/v0.3.1...v0.3.3 194 | [v0.3.1]: https://github.com/m0nhawk/grafana_api/compare/v0.3.0...v0.3.1 195 | [v0.3.0]: https://github.com/m0nhawk/grafana_api/compare/v0.2.9...v0.3.0 196 | [v0.2.9]: https://github.com/m0nhawk/grafana_api/compare/v0.2.8...v0.2.9 197 | [v0.2.8]: https://github.com/m0nhawk/grafana_api/compare/v0.2.6...v0.2.8 198 | [v0.2.6]: https://github.com/m0nhawk/grafana_api/compare/v0.2.2...v0.2.6 199 | [v0.2.2]: https://github.com/m0nhawk/grafana_api/compare/v0.2.1...v0.2.2 200 | [v0.2.1]: https://github.com/m0nhawk/grafana_api/compare/v0.2.0...v0.2.1 201 | [v0.2.0]: https://github.com/m0nhawk/grafana_api/compare/v0.1.7...v0.2.0 202 | [v0.1.7]: https://github.com/m0nhawk/grafana_api/compare/v0.1.6...v0.1.7 203 | [v0.1.6]: https://github.com/m0nhawk/grafana_api/compare/v0.1.5...v0.1.6 204 | [v0.1.5]: https://github.com/m0nhawk/grafana_api/compare/v0.1.4...v0.1.5 205 | [v0.1.4]: https://github.com/m0nhawk/grafana_api/compare/v0.1.3...v0.1.4 206 | [v0.1.3]: https://github.com/m0nhawk/grafana_api/compare/v0.3.6...v0.1.3 207 | -------------------------------------------------------------------------------- /test/api/test_annotations.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | from grafana_api.grafana_api import GrafanaServerError,GrafanaClientError,GrafanaUnauthorizedError,GrafanaBadInputError 7 | 8 | 9 | class AnnotationsTestCase(unittest.TestCase): 10 | def setUp(self): 11 | self.cli = GrafanaFace( 12 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 13 | ) 14 | 15 | @requests_mock.Mocker() 16 | def test_annotations(self, m): 17 | m.get( 18 | "http://localhost/api/annotations?time_from=1563183710618&time_to=1563185212275" 19 | "&alertId=11&dashboardID=111&panelId=22&tags=tags-test&limit=1", 20 | json=[ 21 | { 22 | "id": 80, 23 | "alertId": 11, 24 | "alertName": "", 25 | "dashboardId": 111, 26 | "panelId": 22, 27 | "userId": 0, 28 | "newState": "", 29 | "prevState": "", 30 | "created": 1563280160455, 31 | "updated": 1563280160455, 32 | "time": 1563156456006, 33 | "text": "Annotation Description", 34 | "regionId": 79, 35 | "tags": [ 36 | "tags-test" 37 | ], 38 | "login": "", 39 | "email": "", 40 | "avatarUrl": "", 41 | "data": {} 42 | }, 43 | ] 44 | ) 45 | annotations = self.cli.annotations.get_annotation(time_from=1563183710618, time_to=1563185212275, alert_id=11, 46 | dashboard_id=111, panel_id=22, tags="tags-test", limit=1) 47 | self.assertEqual(annotations[0]["text"], "Annotation Description") 48 | self.assertEqual(annotations[0]["alertId"], 11) 49 | self.assertEqual(annotations[0]["dashboardId"], 111) 50 | self.assertEqual(annotations[0]["panelId"], 22) 51 | self.assertEqual(annotations[0]["tags"][0], "tags-test") 52 | 53 | self.assertEqual(len(annotations), 1) 54 | 55 | @requests_mock.Mocker() 56 | def test_annotations_with_out_param(self, m): 57 | m.get( 58 | "http://localhost/api/annotations", 59 | json=[ 60 | { 61 | "id": 80, 62 | "alertId": 11, 63 | "alertName": "", 64 | "dashboardId": 111, 65 | "panelId": 22, 66 | "userId": 0, 67 | "newState": "", 68 | "prevState": "", 69 | "created": 1563280160455, 70 | "updated": 1563280160455, 71 | "time": 1563156456006, 72 | "text": "Annotation Description", 73 | "regionId": 79, 74 | "tags": [ 75 | "tags-test" 76 | ], 77 | "login": "", 78 | "email": "", 79 | "avatarUrl": "", 80 | "data": {} 81 | }, 82 | ] 83 | ) 84 | annotations = self.cli.annotations.get_annotation() 85 | self.assertEqual(len(annotations), 1) 86 | 87 | @requests_mock.Mocker() 88 | def test_delete_annotations_by_region_id(self, m): 89 | m.delete("http://localhost/api/annotations/region/99", json={"message": "Annotation region deleted"}) 90 | response = self.cli.annotations.delete_annotations_by_region_id(99) 91 | self.assertEqual(response['message'], "Annotation region deleted") 92 | 93 | @requests_mock.Mocker() 94 | def test_delete_annotations_by_id(self, m): 95 | m.delete('http://localhost/api/annotations/99', json={"message": "Annotation deleted"}) 96 | annotation = self.cli.annotations.delete_annotations_by_id(annotations_id=99) 97 | self.assertEqual(annotation['message'], "Annotation deleted") 98 | 99 | @requests_mock.Mocker() 100 | def test_delete_annotations_by_id_could_not_find(self, m): 101 | m.delete("http://localhost/api/annotations/None", json={"message": "Could not find annotation to update"},status_code=500) 102 | response = self.cli.annotations.delete_annotations_by_id(annotations_id=None) 103 | self.assertRaises(GrafanaServerError) 104 | 105 | @requests_mock.Mocker() 106 | def test_delete_annotations_by_id_forbidden(self, m): 107 | m.delete("http://localhost/api/annotations/None", json={"message": "Forbidden"}, 108 | status_code=403) 109 | response = self.cli.annotations.delete_annotations_by_id(annotations_id=None) 110 | self.assertRaises(GrafanaClientError) 111 | 112 | @requests_mock.Mocker() 113 | def test_delete_annotations_by_id_unauthorized(self, m): 114 | m.delete("http://localhost/api/annotations/None", json={"message": "Unauthorized"}, 115 | status_code=401) 116 | response = self.cli.annotations.delete_annotations_by_id(annotations_id=None) 117 | self.assertRaises(GrafanaUnauthorizedError) 118 | 119 | @requests_mock.Mocker() 120 | def test_delete_annotations_by_id_bad_input(self, m): 121 | m.delete("http://localhost/api/annotations/None", json={"message": "Bad Input"}, 122 | status_code=400) 123 | response = self.cli.annotations.delete_annotations_by_id(annotations_id=None) 124 | self.assertRaises(GrafanaBadInputError) 125 | 126 | 127 | @requests_mock.Mocker() 128 | def test_add_annotation(self, m): 129 | m.post( 130 | "http://localhost/api/annotations", 131 | json={"endId": 80, "id": 79, "message": "Annotation added"}, 132 | ) 133 | annotation = self.cli.annotations.add_annotation(time_from=1563183710618, time_to=1563185212275 134 | , is_region=True, tags="tags-test", text="Test") 135 | self.assertEqual(annotation["endId"], 80) 136 | self.assertEqual(annotation["id"], 79) 137 | self.assertEqual(annotation["message"], "Annotation added") 138 | 139 | @requests_mock.Mocker() 140 | def test_update_annotation(self, m): 141 | m.put( 142 | "http://localhost/api/annotations", 143 | json={"endId": 80, "id": 79, "message": "Annotation updated"}, 144 | ) 145 | annotation = self.cli.annotations.update_annotation(time_from=1563183710618, time_to=1563185212275 146 | , is_region=True, tags="tags-test", text="Test") 147 | self.assertEqual(annotation["endId"], 80) 148 | self.assertEqual(annotation["id"], 79) 149 | self.assertEqual(annotation["message"], "Annotation updated") 150 | 151 | @requests_mock.Mocker() 152 | def test_add_annotation_graphite(self, m): 153 | m.post( 154 | "http://localhost/api/annotations/graphite", 155 | json={"message": "Graphite annotation added", "id": 1}, 156 | ) 157 | annotation = self.cli.annotations.add_annotation_graphite(what="Event - deploy", tags="deploy, production", 158 | when=1467844481, data="Data") 159 | 160 | self.assertEqual(annotation["id"], 1) 161 | self.assertEqual(annotation["message"], "Graphite annotation added") 162 | -------------------------------------------------------------------------------- /test/api/test_dashboard.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | 7 | 8 | class DashboardTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.cli = GrafanaFace( 11 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 12 | ) 13 | 14 | @requests_mock.Mocker() 15 | def test_get_dashboard(self, m): 16 | m.get( 17 | "http://localhost/api/dashboards/uid/cIBgcSjkk", 18 | json={ 19 | "dashboard": { 20 | "id": 1, 21 | "uid": "cIBgcSjkk", 22 | "title": "Production Overview", 23 | "tags": ["templated"], 24 | "timezone": "browser", 25 | "schemaVersion": 16, 26 | "version": 0 27 | }, 28 | "meta": { 29 | "isStarred": 'false', 30 | "url": "/d/cIBgcSjkk/production-overview", 31 | "slug": "production-overview" 32 | } 33 | } 34 | ) 35 | dashboard = self.cli.dashboard.get_dashboard("cIBgcSjkk") 36 | self.assertEqual(dashboard["dashboard"]["uid"], "cIBgcSjkk") 37 | 38 | @requests_mock.Mocker() 39 | def test_update_dashboard(self, m): 40 | m.post( 41 | "http://localhost/api/dashboards/db", 42 | json={ 43 | "id": 1, 44 | "uid": "cIBgcSjkk", 45 | "url": "/d/cIBgcSjkk/production-overview", 46 | "status": "success", 47 | "version": 1, 48 | "slug": "production-overview" 49 | } 50 | ) 51 | dashboard = self.cli.dashboard.update_dashboard({ 52 | "dashboard": { 53 | "id": 1, 54 | "uid": 'cIBgcSjkk', 55 | "title": "Production Overview", 56 | "tags": ["templated"], 57 | "timezone": "browser", 58 | "schemaVersion": 16, 59 | "version": 0 60 | }, 61 | "folderId": 0, 62 | "overwrite": 'false' 63 | }) 64 | 65 | self.assertEqual(dashboard["uid"], "cIBgcSjkk") 66 | self.assertEqual(dashboard["status"], "success") 67 | 68 | @requests_mock.Mocker() 69 | def test_get_home_dashboard(self, m): 70 | m.get( 71 | "http://localhost/api/dashboards/home", 72 | json={ 73 | "dashboard": { 74 | "editable": 'false', 75 | "hideControls": 'true', 76 | "nav": [ 77 | { 78 | "enable": 'false', 79 | "type": "timepicker" 80 | } 81 | ], 82 | "style": "dark", 83 | "tags": [], 84 | "templating": { 85 | "list": [ 86 | ] 87 | }, 88 | "time": { 89 | }, 90 | "timezone": "browser", 91 | "title": "Home", 92 | "version": 5 93 | }, 94 | "meta": { 95 | "isHome": 'true', 96 | "canSave": 'false', 97 | "canEdit": 'false', 98 | "canStar": 'false', 99 | "url": "", 100 | "expires": "0001-01-01T00:00:00Z", 101 | "created": "0001-01-01T00:00:00Z" 102 | } 103 | } 104 | ) 105 | dashboard = self.cli.dashboard.get_home_dashboard() 106 | self.assertEqual(dashboard["meta"]["isHome"], "true") 107 | 108 | @requests_mock.Mocker() 109 | def test_delete_dashboard(self, m): 110 | m.delete("http://localhost/api/dashboards/uid/cIBgcSjkk", json={"title": "Production Overview"}) 111 | response = self.cli.dashboard.delete_dashboard("cIBgcSjkk") 112 | self.assertEqual(response['title'], "Production Overview") 113 | 114 | @requests_mock.Mocker() 115 | def test_get_dashboards_tags(self, m): 116 | m.get( 117 | "http://localhost/api/dashboards/tags", 118 | json=[ 119 | { 120 | "term": "tag1", 121 | "count": 1 122 | }, 123 | { 124 | "term": "tag2", 125 | "count": 4 126 | } 127 | ] 128 | ) 129 | tags = self.cli.dashboard.get_dashboards_tags() 130 | self.assertEqual(len(tags), 2) 131 | self.assertEqual(tags[0]["term"], "tag1") 132 | 133 | @requests_mock.Mocker() 134 | def test_get_dashboard_permissions(self, m): 135 | m.get( 136 | "http://localhost/api/dashboards/id/1/permissions", 137 | json=[ 138 | { 139 | "id": 1, 140 | "dashboardId": 1, 141 | "created": "2017-06-20T02:00:00+02:00", 142 | "updated": "2017-06-20T02:00:00+02:00", 143 | "userId": 0, 144 | "userLogin": "", 145 | "userEmail": "", 146 | "teamId": 0, 147 | "team": "", 148 | "role": "Viewer", 149 | "permission": 1, 150 | "permissionName": "View", 151 | "uid": "", 152 | "title": "", 153 | "slug": "", 154 | "isFolder": 'false', 155 | "url": "" 156 | }, 157 | { 158 | "id": 2, 159 | "dashboardId": 1, 160 | "created": "2017-06-20T02:00:00+02:00", 161 | "updated": "2017-06-20T02:00:00+02:00", 162 | "userId": 0, 163 | "userLogin": "", 164 | "userEmail": "", 165 | "teamId": 0, 166 | "team": "", 167 | "role": "Editor", 168 | "permission": 2, 169 | "permissionName": "Edit", 170 | "uid": "", 171 | "title": "", 172 | "slug": "", 173 | "isFolder": 'false', 174 | "url": "" 175 | } 176 | ] 177 | ) 178 | permissions = self.cli.dashboard.get_dashboard_permissions(1) 179 | self.assertEqual(len(permissions), 2) 180 | self.assertEqual(permissions[0]["dashboardId"], 1) 181 | 182 | @requests_mock.Mocker() 183 | def test_update_dashboard_permissions(self, m): 184 | m.post( 185 | "http://localhost/api/dashboards/id/1/permissions", 186 | json={"message": "Dashboard permissions updated"} 187 | ) 188 | permissions = self.cli.dashboard.update_dashboard_permissions(1,{ 189 | "items": [ 190 | { 191 | "role": "Viewer", 192 | "permission": 1 193 | }, 194 | { 195 | "role": "Editor", 196 | "permission": 2 197 | }, 198 | { 199 | "teamId": 1, 200 | "permission": 1 201 | }, 202 | { 203 | "userId": 11, 204 | "permission": 4 205 | } 206 | ] 207 | }) 208 | self.assertEqual(permissions['message'], "Dashboard permissions updated") 209 | -------------------------------------------------------------------------------- /test/api/test_team.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | 7 | 8 | class TeamsTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.cli = GrafanaFace( 11 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 12 | ) 13 | 14 | @requests_mock.Mocker() 15 | def test_search_teams_url_encodes_query(self, m): 16 | m.get( 17 | "http://localhost/api/teams/search?query=my%20team&page=1", 18 | json={ 19 | "totalCount": 1, 20 | "teams": [ 21 | { 22 | "id": 1, 23 | "orgId": 1, 24 | "name": "MyTestTeam", 25 | "email": "", 26 | "avatarUrl": "/avatar/3f49c15916554246daa714b9bd0ee398", 27 | "memberCount": 1, 28 | } 29 | ], 30 | "page": 1, 31 | "perPage": 1000, 32 | }, 33 | ) 34 | teams = self.cli.teams.search_teams("my team") 35 | self.assertEqual(teams[0]["name"], "MyTestTeam") 36 | self.assertEqual(len(teams), 1) 37 | 38 | @requests_mock.Mocker() 39 | def test_search_teams_loads_all_pages(self, m): 40 | m.get( 41 | "http://localhost/api/teams/search?query=team&page=1&perpage=1", 42 | json={ 43 | "totalCount": 2, 44 | "teams": [ 45 | { 46 | "id": 1, 47 | "orgId": 1, 48 | "name": "MyTestTeam", 49 | "email": "", 50 | "avatarUrl": "/avatar/3f49c15916554246daa714b9bd0ee398", 51 | "memberCount": 1, 52 | } 53 | ], 54 | "page": 1, 55 | "perPage": 1, 56 | }, 57 | ) 58 | 59 | m.get( 60 | "http://localhost/api/teams/search?query=team&page=2&perpage=1", 61 | json={ 62 | "totalCount": 2, 63 | "teams": [ 64 | { 65 | "id": 2, 66 | "orgId": 1, 67 | "name": "SecondTeam", 68 | "email": "", 69 | "avatarUrl": "/avatar/3f49c15916554246daa714b9bd0ee398", 70 | "memberCount": 23, 71 | } 72 | ], 73 | "page": 2, 74 | "perPage": 1, 75 | }, 76 | ) 77 | teams = self.cli.teams.search_teams("team",perpage=1) 78 | self.assertEqual(teams[0]["name"], "MyTestTeam") 79 | self.assertEqual(teams[1]["name"], "SecondTeam") 80 | self.assertEqual(len(teams), 2) 81 | 82 | @requests_mock.Mocker() 83 | def test_search_teams_only_loads_requested_page(self, m): 84 | m.get( 85 | "http://localhost/api/teams/search?query=my%20team&page=2", 86 | json={ 87 | "totalCount": 10, 88 | "teams": [ 89 | { 90 | "id": 2, 91 | "orgId": 1, 92 | "name": "MyTestTeam", 93 | "email": "", 94 | "avatarUrl": "/avatar/3f49c15916554246daa714b9bd0ee398", 95 | "memberCount": 1, 96 | } 97 | ], 98 | "page": 1, 99 | "perPage": 1, 100 | }, 101 | ) 102 | teams = self.cli.teams.search_teams("my team", 2) 103 | self.assertEqual(teams[0]["name"], "MyTestTeam") 104 | self.assertEqual(len(teams), 1) 105 | 106 | @requests_mock.Mocker() 107 | def test_get_team_by_name(self, m): 108 | m.get( 109 | "http://localhost/api/teams/search?name=my%20team", 110 | json={ 111 | "totalCount": 1, 112 | "teams": [ 113 | { 114 | "id": 2, 115 | "orgId": 1, 116 | "name": "my team", 117 | "email": "", 118 | "avatarUrl": "/avatar/3f49c15916554246daa714b9bd0ee398", 119 | "memberCount": 1, 120 | } 121 | ], 122 | "page": 1, 123 | "perPage": 1000, 124 | }, 125 | ) 126 | teams = self.cli.teams.get_team_by_name("my team") 127 | self.assertEqual(teams[0]["name"], "my team") 128 | self.assertEqual(len(teams), 1) 129 | 130 | @requests_mock.Mocker() 131 | def test_get_team(self, m): 132 | m.get( 133 | "http://localhost/api/teams/1", 134 | json={ 135 | "id": 1, 136 | "orgId": 1, 137 | "name": "MyTestTeam", 138 | "email": "", 139 | "created": "2017-12-15T10:40:45+01:00", 140 | "updated": "2017-12-15T10:40:45+01:00", 141 | }, 142 | ) 143 | team = self.cli.teams.get_team("1") 144 | self.assertEqual(team["name"], "MyTestTeam") 145 | 146 | @requests_mock.Mocker() 147 | def test_add_team(self, m): 148 | m.post( 149 | "http://localhost/api/teams", json={"message": "Team created", "teamId": 2} 150 | ) 151 | team = {"name": "MySecondTestTeam", "email": "email@test.com"} 152 | new_team = self.cli.teams.add_team(team) 153 | self.assertEqual(new_team["teamId"], 2) 154 | 155 | @requests_mock.Mocker() 156 | def test_update_team(self, m): 157 | m.put("http://localhost/api/teams/3", json={"message": "Team updated"}) 158 | team = {"name": "MyThirdTestTeam", "email": "email@test.com"} 159 | response = self.cli.teams.update_team(3, team) 160 | self.assertEqual(response["message"], "Team updated") 161 | 162 | @requests_mock.Mocker() 163 | def test_delete_team(self, m): 164 | m.delete("http://localhost/api/teams/3", json={"message": "Team deleted"}) 165 | response = self.cli.teams.delete_team(3) 166 | self.assertEqual(response, True) 167 | 168 | @requests_mock.Mocker() 169 | def test_get_team_members(self, m): 170 | m.get( 171 | "http://localhost/api/teams/1/members", 172 | json=[ 173 | { 174 | "orgId": 1, 175 | "teamId": 1, 176 | "userId": 3, 177 | "email": "user1@email.com", 178 | "login": "user1", 179 | "avatarUrl": "/avatar/1b3c32f6386b0185c40d359cdc733a79", 180 | } 181 | ], 182 | ) 183 | members = self.cli.teams.get_team_members("1") 184 | self.assertEqual(members[0]["login"], "user1") 185 | 186 | @requests_mock.Mocker() 187 | def test_add_team_member(self, m): 188 | m.post( 189 | "http://localhost/api/teams/1/members", 190 | json={"message": "Member added to Team"}, 191 | ) 192 | history = m.request_history 193 | add_res = self.cli.teams.add_team_member("1", "3") 194 | self.assertEqual(history[0].json()["userId"], "3") 195 | self.assertEqual(add_res["message"], "Member added to Team") 196 | 197 | @requests_mock.Mocker() 198 | def test_remove_team_member(self, m): 199 | m.delete( 200 | "http://localhost/api/teams/13/members/2", 201 | json={"message": "Team member removed"}, 202 | ) 203 | remove_res = self.cli.teams.remove_team_member("13", "2") 204 | self.assertEqual(remove_res["message"], "Team member removed") 205 | 206 | @requests_mock.Mocker() 207 | def test_get_team_preferences(self, m): 208 | m.get( 209 | "http://localhost/api/teams/1/preferences", 210 | json={"theme": "", "homeDashboardId": 0, "timezone": ""}, 211 | ) 212 | prefs = self.cli.teams.get_team_preferences("1") 213 | self.assertEqual(prefs["homeDashboardId"], 0) 214 | 215 | @requests_mock.Mocker() 216 | def test_update_team_preferences(self, m): 217 | m.put( 218 | "http://localhost/api/teams/1/preferences", 219 | json={"message": "Preferences updated"}, 220 | ) 221 | prefs = {"theme": "light", "homeDashboardId": 0, "timezone": ""} 222 | 223 | updates = self.cli.teams.update_team_preferences("1", prefs) 224 | history = m.request_history 225 | json_payload = history[0].json() 226 | self.assertEqual(json_payload["theme"], "light") 227 | self.assertEqual(updates["message"], "Preferences updated") 228 | -------------------------------------------------------------------------------- /test/api/test_organization.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | 7 | 8 | class OrganizationTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.cli = GrafanaFace( 11 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 12 | ) 13 | 14 | @requests_mock.Mocker() 15 | def test_delete_snapshot_by_key(self, m): 16 | m.delete('http://localhost/api/orgs/1/users/2', json={"message": "User removed from organization"}) 17 | annotation = self.cli.organizations.organization_user_delete(organization_id=1, user_id=2) 18 | self.assertEqual(annotation['message'], "User removed from organization") 19 | 20 | @requests_mock.Mocker() 21 | def test_organization_preference_get(self, m): 22 | m.get( 23 | "http://localhost/api/org/preferences", 24 | json={"theme": "", "homeDashboardId": 0, "timezone": ""} 25 | ) 26 | 27 | result = self.cli.organizations.organization_preference_get() 28 | self.assertEqual(result["homeDashboardId"], 0) 29 | 30 | @requests_mock.Mocker() 31 | def test_organization_preference_update(self, m): 32 | m.put( 33 | "http://localhost/api/org/preferences", 34 | json={"message": "Preferences updated"} 35 | ) 36 | preference = self.cli.organizations.organization_preference_update(theme="", home_dashboard_id=0, 37 | timezone="utc") 38 | self.assertEqual(preference["message"], "Preferences updated") 39 | 40 | @requests_mock.Mocker() 41 | def test_organization_user_update(self, m): 42 | m.patch( 43 | "http://localhost/api/orgs/1/users/2", 44 | json={"message": "Organization user updated"} 45 | ) 46 | preference = self.cli.organizations.organization_user_update(organization_id=1, user_id=2, user_role="Admin") 47 | self.assertEqual(preference["message"], "Organization user updated") 48 | 49 | @requests_mock.Mocker() 50 | def test_organization_user_add(self, m): 51 | m.post( 52 | "http://localhost/api/orgs/1/users", 53 | json={"message": "User added to organization"} 54 | ) 55 | preference = self.cli.organizations.organization_user_add(organization_id=1, user={ 56 | "loginOrEmail": "user", 57 | "role": "Viewer" 58 | }) 59 | self.assertEqual(preference["message"], "User added to organization") 60 | 61 | @requests_mock.Mocker() 62 | def test_organization_user_list(self, m): 63 | m.get( 64 | "http://localhost/api/orgs/1/users", 65 | json=[ 66 | { 67 | "orgId": 1, 68 | "userId": 1, 69 | "email": "admin@mygraf.com", 70 | "login": "admin", 71 | "role": "Admin" 72 | } 73 | ] 74 | ) 75 | users = self.cli.organizations.organization_user_list(organization_id=1) 76 | self.assertEqual(len(users), 1) 77 | 78 | @requests_mock.Mocker() 79 | def test_list_organization(self, m): 80 | m.get( 81 | "http://localhost/api/orgs", 82 | json=[ 83 | { 84 | "orgId": 1, 85 | "userId": 1, 86 | "email": "admin@mygraf.com", 87 | "login": "admin", 88 | "role": "Admin" 89 | } 90 | ] 91 | ) 92 | users = self.cli.organizations.list_organization() 93 | self.assertEqual(len(users), 1) 94 | 95 | @requests_mock.Mocker() 96 | def test_get_current_organization(self, m): 97 | m.get( 98 | "http://localhost/api/org", 99 | json={ 100 | "id": 1, 101 | "name": "Main Org." 102 | } 103 | ) 104 | orgs = self.cli.organization.get_current_organization() 105 | self.assertEqual(orgs['name'], "Main Org.") 106 | 107 | @requests_mock.Mocker() 108 | def test_update_current_organization(self, m): 109 | m.put( 110 | "http://localhost/api/org", 111 | json={"message": "Organization updated"} 112 | ) 113 | org = self.cli.organization.update_current_organization(organization={ 114 | "name": "Main Org." 115 | }) 116 | self.assertEqual(org['message'], "Organization updated") 117 | 118 | @requests_mock.Mocker() 119 | def test_update_organization(self, m): 120 | m.put( 121 | "http://localhost/api/orgs/1", 122 | json={"message": "Organization updated"} 123 | ) 124 | preference = self.cli.organizations.update_organization(organization_id=1, organization={ 125 | "name": "Main Org 2." 126 | }) 127 | self.assertEqual(preference["message"], "Organization updated") 128 | 129 | @requests_mock.Mocker() 130 | def test_delete_organization(self, m): 131 | m.delete( 132 | "http://localhost/api/orgs/1", 133 | json={"message": "Organization deleted"} 134 | ) 135 | preference = self.cli.organizations.delete_organization(organization_id=1) 136 | self.assertEqual(preference["message"], "Organization deleted") 137 | 138 | @requests_mock.Mocker() 139 | def test_create_organization(self, m): 140 | m.post( 141 | "http://localhost/api/orgs", 142 | json={ 143 | "orgId": "1", 144 | "message": "Organization created" 145 | } 146 | ) 147 | preference = self.cli.organization.create_organization(organization={ 148 | "name": "New Org." 149 | }) 150 | self.assertEqual(preference["message"], "Organization created") 151 | 152 | @requests_mock.Mocker() 153 | def test_delete_user_current_organization(self, m): 154 | m.delete( 155 | "http://localhost/api/org/users/1", 156 | json={"message": "User removed from organization"} 157 | ) 158 | preference = self.cli.organization.delete_user_current_organization(user_id=1) 159 | self.assertEqual(preference["message"], "User removed from organization") 160 | 161 | @requests_mock.Mocker() 162 | def test_add_user_current_organization(self, m): 163 | m.post( 164 | "http://localhost/api/org/users", 165 | json={"message": "User added to organization"} 166 | ) 167 | preference = self.cli.organization.add_user_current_organization({ 168 | "role": "Admin", 169 | "loginOrEmail": "admin" 170 | }) 171 | self.assertEqual(preference["message"], "User added to organization") 172 | 173 | @requests_mock.Mocker() 174 | def test_update_user_current_organization(self, m): 175 | m.patch( 176 | "http://localhost/api/org/users/1", 177 | json={"message": "Organization user updated"} 178 | ) 179 | preference = self.cli.organization.update_user_current_organization(user_id=1, user={ 180 | "role": "Viewer", 181 | }) 182 | self.assertEqual(preference["message"], "Organization user updated") 183 | 184 | @requests_mock.Mocker() 185 | def test_get_current_organization_users(self, m): 186 | m.get( 187 | "http://localhost/api/org/users", 188 | json=[ 189 | { 190 | "orgId": 1, 191 | "userId": 1, 192 | "email": "admin@mygraf.com", 193 | "login": "admin", 194 | "role": "Admin" 195 | } 196 | ] 197 | ) 198 | org = self.cli.organization.get_current_organization_users() 199 | self.assertEqual(len(org), 1) 200 | 201 | @requests_mock.Mocker() 202 | def test_find_organization(self, m): 203 | m.get( 204 | "http://localhost/api/orgs/name/Main", 205 | json= 206 | { 207 | "id": 1, 208 | "name": "Main Org.", 209 | "address": { 210 | "address1": "", 211 | "address2": "", 212 | "city": "", 213 | "zipCode": "", 214 | "state": "", 215 | "country": "" 216 | } 217 | } 218 | ) 219 | org = self.cli.organization.find_organization(org_name="Main") 220 | self.assertEqual(org['id'], 1) 221 | 222 | @requests_mock.Mocker() 223 | def test_switch_organization(self, m): 224 | m.post( 225 | "http://localhost/api/user/using/2", 226 | json={"message":"Active organization changed"} 227 | ) 228 | preference = self.cli.organizations.switch_organization(organization_id=2) 229 | self.assertEqual(preference["message"], "Active organization changed") 230 | 231 | 232 | -------------------------------------------------------------------------------- /test/api/test_admin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | 7 | 8 | class AdminTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.cli = GrafanaFace( 11 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 12 | ) 13 | 14 | @requests_mock.Mocker() 15 | def test_settings(self, m): 16 | m.get( 17 | "http://localhost/api/admin/settings", 18 | json={ 19 | "DEFAULT": { 20 | "app_mode": "production" 21 | }, 22 | "analytics": { 23 | "google_analytics_ua_id": "", 24 | "reporting_enabled": "false" 25 | }, 26 | "auth.anonymous": { 27 | "enabled": "true", 28 | "org_name": "Main Org.", 29 | "org_role": "Viewer" 30 | }, 31 | "auth.basic": { 32 | "enabled": "false" 33 | }, 34 | "auth.github": { 35 | "allow_sign_up": "false", 36 | "allowed_domains": "", 37 | "allowed_organizations": "", 38 | "api_url": "https://api.github.com/user", 39 | "auth_url": "https://github.com/login/oauth/authorize", 40 | "client_id": "some_id", 41 | "client_secret": "************", 42 | "enabled": "false", 43 | "scopes": "user:email,read:org", 44 | "team_ids": "", 45 | "token_url": "https://github.com/login/oauth/access_token" 46 | }, 47 | "auth.google": { 48 | "allow_sign_up": "false", "allowed_domains": "", 49 | "api_url": "https://www.googleapis.com/oauth2/v1/userinfo", 50 | "auth_url": "https://accounts.google.com/o/oauth2/auth", 51 | "client_id": "some_client_id", 52 | "client_secret": "************", 53 | "enabled": "false", 54 | "scopes": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", 55 | "token_url": "https://accounts.google.com/o/oauth2/token" 56 | }, 57 | "auth.ldap": { 58 | "config_file": "/etc/grafana/ldap.toml", 59 | "enabled": "false" 60 | }, 61 | "auth.proxy": { 62 | "auto_sign_up": "true", 63 | "enabled": "false", 64 | "header_name": "X-WEBAUTH-USER", 65 | "header_property": "username" 66 | }, 67 | "dashboards.json": { 68 | "enabled": "false", 69 | "path": "/var/lib/grafana/dashboards" 70 | }, 71 | "database": { 72 | "host": "127.0.0.1:0000", 73 | "name": "grafana", 74 | "password": "************", 75 | "path": "grafana.db", 76 | "ssl_mode": "disable", 77 | "type": "sqlite3", 78 | "user": "root" 79 | }, 80 | "emails": { 81 | "templates_pattern": "emails/*.html", 82 | "welcome_email_on_sign_up": "false" 83 | }, 84 | "log": { 85 | "buffer_len": "10000", 86 | "level": "Info", 87 | "mode": "file" 88 | }, 89 | "log.console": { 90 | "level": "" 91 | }, 92 | "log.file": { 93 | "daily_rotate": "true", 94 | "file_name": "", 95 | "level": "", 96 | "log_rotate": "true", 97 | "max_days": "7", 98 | "max_lines": "1000000", 99 | "max_lines_shift": "28", 100 | "max_size_shift": "" 101 | }, 102 | "paths": { 103 | "data": "/tsdb/grafana", 104 | "logs": "/logs/apps/grafana"}, 105 | "security": { 106 | "admin_password": "************", 107 | "admin_user": "admin", 108 | "cookie_remember_name": "grafana_remember", 109 | "cookie_username": "grafana_user", 110 | "disable_gravatar": "false", 111 | "login_remember_days": "7", 112 | "secret_key": "************" 113 | }, 114 | "server": { 115 | "cert_file": "", 116 | "cert_key": "", 117 | "domain": "mygraf.com", 118 | "enable_gzip": "false", 119 | "enforce_domain": "false", 120 | "http_addr": "127.0.0.1", 121 | "http_port": "0000", 122 | "protocol": "http", 123 | "root_url": "%(protocol)s://%(domain)s:%(http_port)s/", 124 | "router_logging": "true", 125 | "data_proxy_logging": "true", 126 | "static_root_path": "public" 127 | }, 128 | "session": { 129 | "cookie_name": "grafana_sess", 130 | "cookie_secure": "false", 131 | "gc_interval_time": "", 132 | "provider": "file", 133 | "provider_config": "sessions", 134 | "session_life_time": "86400" 135 | }, 136 | "smtp": { 137 | "cert_file": "", 138 | "enabled": "false", 139 | "from_address": "admin@grafana.localhost", 140 | "from_name": "Grafana", 141 | "ehlo_identity": "dashboard.example.com", 142 | "host": "localhost:25", 143 | "key_file": "", 144 | "password": "************", 145 | "skip_verify": "false", 146 | "user": "" 147 | }, 148 | "users": { 149 | "allow_org_create": "true", 150 | "allow_sign_up": "false", 151 | "auto_assign_org": "true", 152 | "auto_assign_org_role": "Viewer" 153 | } 154 | } 155 | ) 156 | admin = self.cli.admin.settings() 157 | self.assertEqual(admin["users"]["allow_org_create"], "true") 158 | 159 | @requests_mock.Mocker() 160 | def test_stats(self, m): 161 | m.get( 162 | "http://localhost/api/admin/stats", 163 | json={ 164 | "users": 2, 165 | "orgs": 1, 166 | "dashboards": 4, 167 | "snapshots": 2, 168 | "tags": 6, 169 | "datasources": 1, 170 | "playlists": 1, 171 | "stars": 2, 172 | "alerts": 2, 173 | "activeUsers": 1 174 | } 175 | ) 176 | stats = self.cli.admin.stats() 177 | self.assertEqual(len(stats), 10) 178 | 179 | @requests_mock.Mocker() 180 | def test_create_user(self, m): 181 | m.post( 182 | "http://localhost/api/admin/users", 183 | json={"id": 5, "message": "User created"} 184 | ) 185 | user = self.cli.admin.create_user({ 186 | "name": "User", 187 | "email": "user@graf.com", 188 | "login": "user", 189 | "password": "userpassword" 190 | }) 191 | self.assertEqual(user['message'], "User created") 192 | 193 | @requests_mock.Mocker() 194 | def test_change_user_password(self, m): 195 | m.put( 196 | "http://localhost/api/admin/users/2/password", 197 | json={"message": "User password updated"} 198 | ) 199 | user = self.cli.admin.change_user_password(user_id=2, password="password") 200 | self.assertEqual(user['message'], "User password updated") 201 | 202 | @requests_mock.Mocker() 203 | def test_change_user_permissions(self, m): 204 | m.put( 205 | "http://localhost/api/admin/users/2/permissions", 206 | json={"message": "User permissions updated"} 207 | ) 208 | user = self.cli.admin.change_user_permissions(user_id=2, is_grafana_admin=True) 209 | self.assertEqual(user['message'], "User permissions updated") 210 | 211 | @requests_mock.Mocker() 212 | def test_delete_user(self, m): 213 | m.delete( 214 | "http://localhost/api/admin/users/2", 215 | json={"message": "User deleted"} 216 | ) 217 | user = self.cli.admin.delete_user(user_id=2) 218 | self.assertEqual(user['message'], "User deleted") 219 | 220 | @requests_mock.Mocker() 221 | def test_pause_all_alerts(self, m): 222 | m.post( 223 | "http://localhost/api/admin/pause-all-alerts", 224 | json={ 225 | "state": "Paused", 226 | "message": "alert paused", 227 | "alertsAffected": 1 228 | } 229 | ) 230 | pause = self.cli.admin.pause_all_alerts(pause='True') 231 | self.assertEqual(pause['message'], "alert paused") 232 | -------------------------------------------------------------------------------- /test/api/test_folder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | from grafana_api.grafana_face import GrafanaFace 6 | from grafana_api.grafana_api import GrafanaBadInputError 7 | 8 | 9 | class FolderTestCase(unittest.TestCase): 10 | def setUp(self): 11 | self.cli = GrafanaFace( 12 | ("admin", "admin"), host="localhost", url_path_prefix="", protocol="http" 13 | ) 14 | 15 | @requests_mock.Mocker() 16 | def test_get_all_folders(self, m): 17 | m.get( 18 | "http://localhost/api/folders", 19 | json=[ 20 | { 21 | "id": 1, 22 | "uid": "nErXDvCkzz", 23 | "title": "Departmenet ABC", 24 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 25 | "hasAcl": "false", 26 | "canSave": "false", 27 | "canEdit": "false", 28 | "canAdmin": "false", 29 | "createdBy": "admin", 30 | "created": "2018-01-31T17:43:12+01:00", 31 | "updatedBy": "admin", 32 | "updated": "2018-01-31T17:43:12+01:00", 33 | "version": 1 34 | } 35 | ] 36 | ) 37 | folders = self.cli.folder.get_all_folders() 38 | self.assertEqual(folders[0]["id"], 1) 39 | self.assertEqual(len(folders), 1) 40 | 41 | @requests_mock.Mocker() 42 | def test_get_folder(self, m): 43 | m.get( 44 | "http://localhost/api/folders/nErXDvCkzzh", 45 | json={ 46 | "id": 1, 47 | "uid": "nErXDvCkzzh", 48 | "title": "Departmenet ABC", 49 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 50 | "hasAcl": "false", 51 | "canSave": "false", 52 | "canEdit": "false", 53 | "canAdmin": "false", 54 | "createdBy": "admin", 55 | "created": "2018-01-31T17:43:12+01:00", 56 | "updatedBy": "admin", 57 | "updated": "2018-01-31T17:43:12+01:00", 58 | "version": 1 59 | } 60 | ) 61 | folders = self.cli.folder.get_folder(uid="nErXDvCkzzh") 62 | self.assertEqual(folders["uid"], "nErXDvCkzzh") 63 | 64 | @requests_mock.Mocker() 65 | def test_create_folder(self, m): 66 | m.post( 67 | "http://localhost/api/folders", 68 | json={ 69 | "id": 1, 70 | "uid": "nErXDvCkzz", 71 | "title": "Departmenet ABC", 72 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 73 | "hasAcl": "false", 74 | "canSave": "false", 75 | "canEdit": "false", 76 | "canAdmin": "false", 77 | "createdBy": "admin", 78 | "created": "2018-01-31T17:43:12+01:00", 79 | "updatedBy": "admin", 80 | "updated": "2018-01-31T17:43:12+01:00", 81 | "version": 1 82 | } 83 | ) 84 | folder = self.cli.folder.create_folder(title="Departmenet ABC", uid="nErXDvCkzz") 85 | self.assertEqual(folder["uid"], "nErXDvCkzz") 86 | 87 | @requests_mock.Mocker() 88 | def test_create_folder_empty_uid(self, m): 89 | m.post( 90 | "http://localhost/api/folders", 91 | json={ 92 | "message": "Folder title cannot be empty" 93 | }, status_code=400 94 | ) 95 | folder = self.cli.folder.create_folder(title="Departmenet ABC") 96 | self.assertRaises(GrafanaBadInputError) 97 | 98 | @requests_mock.Mocker() 99 | def test_update_folder(self, m): 100 | m.put( 101 | "http://localhost/api/folders/nErXDvCkzz", 102 | json={ 103 | "id": 1, 104 | "uid": "nErXDvCkzz", 105 | "title": "Departmenet DEF", 106 | "url": "/dashboards/f/nErXDvCkzz/department-def", 107 | "hasAcl": "false", 108 | "canSave": "false", 109 | "canEdit": "false", 110 | "canAdmin": "false", 111 | "createdBy": "admin", 112 | "created": "2018-01-31T17:43:12+01:00", 113 | "updatedBy": "admin", 114 | "updated": "2018-01-31T17:43:12+01:00", 115 | "version": 1 116 | } 117 | ) 118 | folder = self.cli.folder.update_folder(title="Departmenet DEF", uid="nErXDvCkzz", version=1, overwrite=True) 119 | self.assertEqual(folder["title"], "Departmenet DEF") 120 | 121 | @requests_mock.Mocker() 122 | def test_update_folder_some_param(self, m): 123 | m.put( 124 | "http://localhost/api/folders/nErXDvCkzz", 125 | json={ 126 | "id": 1, 127 | "uid": "nErXDvCkzz", 128 | "title": "Departmenet DEF", 129 | "url": "/dashboards/f/nErXDvCkzz/department-def", 130 | "hasAcl": "false", 131 | "canSave": "false", 132 | "canEdit": "false", 133 | "canAdmin": "false", 134 | "createdBy": "admin", 135 | "created": "2018-01-31T17:43:12+01:00", 136 | "updatedBy": "admin", 137 | "updated": "2018-01-31T17:43:12+01:00", 138 | "version": 1 139 | } 140 | ) 141 | folder = self.cli.folder.update_folder(title="Departmenet DEF", uid="nErXDvCkzz") 142 | self.assertEqual(folder["title"], "Departmenet DEF") 143 | 144 | @requests_mock.Mocker() 145 | def test_get_folder_by_id(self, m): 146 | m.get( 147 | "http://localhost/api/folders/id/1", 148 | json={ 149 | "id": 1, 150 | "uid": "nErXDvCkzz", 151 | "title": "Departmenet ABC", 152 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 153 | "hasAcl": "false", 154 | "canSave": "false", 155 | "canEdit": "false", 156 | "canAdmin": "false", 157 | "createdBy": "admin", 158 | "created": "2018-01-31T17:43:12+01:00", 159 | "updatedBy": "admin", 160 | "updated": "2018-01-31T17:43:12+01:00", 161 | "version": 1 162 | } 163 | ) 164 | folder = self.cli.folder.get_folder_by_id(folder_id=1) 165 | self.assertEqual(folder["id"], 1) 166 | 167 | @requests_mock.Mocker() 168 | def test_get_folder_permissions(self, m): 169 | m.get( 170 | "http://localhost/api/folders/nErXDvCkzz/permissions", 171 | json=[ 172 | { 173 | "id": 1, 174 | "folderId": -1, 175 | "created": "2017-06-20T02:00:00+02:00", 176 | "updated": "2017-06-20T02:00:00+02:00", 177 | "userId": 0, 178 | "userLogin": "", 179 | "userEmail": "", 180 | "teamId": 0, 181 | "team": "", 182 | "role": "Viewer", 183 | "permission": 1, 184 | "permissionName": "View", 185 | "uid": "nErXDvCkzz", 186 | "title": "", 187 | "slug": "", 188 | "isFolder": "false", 189 | "url": "" 190 | }, 191 | { 192 | "id": 2, 193 | "dashboardId": -1, 194 | "created": "2017-06-20T02:00:00+02:00", 195 | "updated": "2017-06-20T02:00:00+02:00", 196 | "userId": 0, 197 | "userLogin": "", 198 | "userEmail": "", 199 | "teamId": 0, 200 | "team": "", 201 | "role": "Editor", 202 | "permission": 2, 203 | "permissionName": "Edit", 204 | "uid": "", 205 | "title": "", 206 | "slug": "", 207 | "isFolder": "false", 208 | "url": "" 209 | } 210 | ] 211 | ) 212 | folder_permissions = self.cli.folder.get_folder_permissions(uid="nErXDvCkzz") 213 | self.assertEqual(folder_permissions[0]["permissionName"], "View") 214 | 215 | @requests_mock.Mocker() 216 | def test_update_folder_permissions(self, m): 217 | m.post( 218 | "http://localhost/api/folders/nErXDvCkzz/permissions", 219 | json={"message": "Folder permissions updated"} 220 | ) 221 | folder = self.cli.folder.update_folder_permissions(uid="nErXDvCkzz", items=[ 222 | { 223 | "role": "Viewer", 224 | "permission": 1 225 | }, 226 | { 227 | "role": "Editor", 228 | "permission": 2 229 | }, 230 | { 231 | "teamId": 1, 232 | "permission": 1 233 | }, 234 | { 235 | "userId": 11, 236 | "permission": 4 237 | } 238 | ]) 239 | self.assertEqual(folder["message"], "Folder permissions updated") 240 | 241 | @requests_mock.Mocker() 242 | def test_delete_folder(self, m): 243 | m.delete("http://localhost/api/folders/nErXDvCkzz", json={"message": "Folder deleted"}) 244 | folder = self.cli.folder.delete_folder(uid="nErXDvCkzz") 245 | self.assertEqual(folder['message'], "Folder deleted") 246 | --------------------------------------------------------------------------------