├── docs ├── _static │ └── .gitkeep ├── requirements.txt ├── guide │ ├── index.rst │ ├── install.rst │ └── basic_usage.rst ├── reference │ ├── cachetclient.client.rst │ ├── cachetclient.v1.ping.rst │ ├── index.rst │ ├── cachetclient.v1.version.rst │ ├── cachetclient.v1.schedules.rst │ ├── cachetclient.v1.subscribers.rst │ ├── cachetclient.v1.metric_points.rst │ ├── cachetclient.v1.enums.rst │ ├── cachetclient.v1.metrics.rst │ ├── cachetclient.v1.incident_updates.rst │ ├── cachetclient.v1.incidents.rst │ ├── cachetclient.v1.component_groups.rst │ └── cachetclient.v1.components.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── MANIFEST.in ├── tests ├── requirements.txt ├── base.py ├── test_ping.py ├── test_version.py ├── test_metrics.py ├── test_metric_points.py ├── test_schedules.py ├── test_client.py ├── test_subscribers.py ├── test_incidents.py ├── test_incident_updates.py ├── test_components.py ├── test_docs.py ├── test_component_groups.py └── fakeapi.py ├── .github ├── logo.png └── workflows │ └── pythonpackage.yml ├── extras ├── envclear.sh ├── env23.sh ├── env24.sh ├── release.md ├── README.md └── live_run.py ├── pytest.ini ├── cachetclient ├── __init__.py ├── cli.py ├── v1 │ ├── __init__.py │ ├── ping.py │ ├── client.py │ ├── version.py │ ├── enums.py │ ├── metric_points.py │ ├── subscribers.py │ ├── schedules.py │ ├── incident_updates.py │ ├── metrics.py │ ├── component_groups.py │ ├── incidents.py │ └── components.py ├── utils.py ├── httpclient.py ├── client.py └── base.py ├── .readthedocs.yml ├── .gitignore ├── cachet24.env ├── cachet23.env ├── docker-compose.yaml ├── LICENSE ├── setup.py ├── tox.ini ├── CHANGELOG.md ├── README.md └── .pylintrc /docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==4.3.1 2 | tox==3.8.3 3 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZettaIO/cachet-client/HEAD/.github/logo.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools>=40.2.0 2 | Sphinx==2.1.2 3 | sphinx-rtd-theme==0.4.3 4 | -------------------------------------------------------------------------------- /extras/envclear.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | unset CACHET_ENDPOINT 4 | unset CACHET_API_TOKEN 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | python_files=test_*.py 4 | addopts = -v --verbose 5 | -------------------------------------------------------------------------------- /cachetclient/__init__.py: -------------------------------------------------------------------------------- 1 | from cachetclient.client import Client # noqa 2 | 3 | ___version__ = "4.0.1" 4 | -------------------------------------------------------------------------------- /cachetclient/cli.py: -------------------------------------------------------------------------------- 1 | # import os 2 | import sys 3 | 4 | 5 | def execute_from_command_line(): 6 | print(sys.argv) 7 | -------------------------------------------------------------------------------- /docs/guide/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Guide 3 | ===== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | install 9 | basic_usage 10 | 11 | -------------------------------------------------------------------------------- /extras/env23.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export CACHET_ENDPOINT='http://localhost:8000/api/v1' 4 | export CACHET_API_TOKEN='xwqW5J9IjvC8ERvf3GdU' 5 | -------------------------------------------------------------------------------- /extras/env24.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export CACHET_ENDPOINT='http://localhost:8080/api/v1' 4 | export CACHET_API_TOKEN='bZi1aeZKq3HSNL8XzKFk' 5 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.client.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient 2 | .. py:currentmodule:: cachetclient 3 | 4 | Client 5 | ====== 6 | 7 | .. autofunction:: Client 8 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | image: latest 4 | 5 | 6 | python: 7 | version: 3.7 8 | setup_py_install: true 9 | 10 | requirements_file: docs/requirements.txt 11 | -------------------------------------------------------------------------------- /extras/release.md: -------------------------------------------------------------------------------- 1 | # Creating a release 2 | 3 | - Bump version in `setup.py`, `__init__.py` and `docs/conf.py` 4 | - run `tox` or ensure CI passed 5 | - Ensure docs are updated 6 | - `twine upload dist/...` 7 | - Create release in github 8 | - Ensure new docs are built on RTD 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | __pycache__ 3 | *.pyc 4 | *egg-info* 5 | /dist/ 6 | /build/ 7 | /docs/_build 8 | .pytest_cache/ 9 | 10 | 11 | # IDE 12 | .idea 13 | .vscode 14 | 15 | # Virtualenvs 16 | .venv 17 | .venv2 18 | .env 19 | env 20 | venv 21 | 22 | # Test and docs 23 | .tox 24 | 25 | # project 26 | extras/env.sh 27 | test.py 28 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | """Simplify all tests""" 2 | from unittest import TestCase, mock 3 | 4 | import cachetclient 5 | from fakeapi import FakeHttpClient 6 | 7 | 8 | class CachetTestcase(TestCase): 9 | endpoint = 'https://status.example.com/api/v1' 10 | token = 's4cr337k33y' 11 | 12 | def create_client(self) -> cachetclient.v1.Client: 13 | return cachetclient.Client(endpoint=self.endpoint, api_token=self.token) 14 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.ping.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.ping 2 | .. py:currentmodule:: cachetclient.v1.ping 3 | 4 | Ping 5 | ==== 6 | 7 | Methods 8 | ------- 9 | 10 | .. automethod:: PingManager.__init__ 11 | .. automethod:: PingManager.__call__ 12 | .. automethod:: PingManager.get 13 | 14 | Attributes 15 | ---------- 16 | 17 | .. autoattribute:: PingManager.path 18 | .. autoattribute:: PingManager.resource_class 19 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Reference 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | cachetclient.client 9 | cachetclient.v1.enums 10 | cachetclient.v1.ping 11 | cachetclient.v1.version 12 | cachetclient.v1.subscribers 13 | cachetclient.v1.components 14 | cachetclient.v1.component_groups 15 | cachetclient.v1.incidents 16 | cachetclient.v1.incident_updates 17 | cachetclient.v1.metrics 18 | cachetclient.v1.metric_points 19 | cachetclient.v1.schedules 20 | -------------------------------------------------------------------------------- /cachet24.env: -------------------------------------------------------------------------------- 1 | 2 | DOCKER=true 3 | APP_DEBUG=true 4 | DEBUG=true 5 | APP_LOG=errorlog 6 | APP_URL=http://localhost:8080 7 | QUEUE_DRIVER=database 8 | APP_KEY=base64:lqQi3QCptM5ZykMkuzk8WD4hZJ4XRs3+z271SO3dZK4= 9 | 10 | DB_DRIVER=mysql 11 | DB_HOST=cachet24_db 12 | DB_DATABASE=status 13 | DB_USERNAME=test 14 | DB_PASSWORD=test 15 | 16 | MAIL_DRIVER=log 17 | MAIL_HOST=null 18 | MAIL_PORT=null 19 | MAIL_USERNAME=test 20 | MAIL_PASSWORD=test 21 | MAIL_ADDRESS=test@test 22 | MAIL_NAME=Test 23 | MAIL_ENCRYPTION=tls 24 | -------------------------------------------------------------------------------- /cachet23.env: -------------------------------------------------------------------------------- 1 | 2 | DOCKER=true 3 | APP_DEBUG=true 4 | DEBUG=true 5 | APP_LOG="errorlog" 6 | APP_URL=http://localhost:8000 7 | QUEUE_DRIVER="database" 8 | APP_KEY=base64:lrpz03UggowQfOuHDEs1jPMlLGwf2k12p59PQt6+Xms= 9 | 10 | DB_DRIVER=mysql 11 | DB_HOST=cachet23_db 12 | DB_DATABASE=status 13 | DB_USERNAME=test 14 | DB_PASSWORD=test 15 | 16 | MAIL_DRIVER=log 17 | MAIL_HOST=null 18 | MAIL_PORT=null 19 | MAIL_USERNAME=test 20 | MAIL_PASSWORD=test 21 | MAIL_ADDRESS=test@test.test 22 | MAIL_NAME=Test 23 | MAIL_ENCRYPTION=tls 24 | -------------------------------------------------------------------------------- /cachetclient/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from cachetclient.v1.client import Client # noqa 2 | 3 | from cachetclient.v1.subscribers import Subscriber # noqa 4 | from cachetclient.v1.components import Component # noqa 5 | from cachetclient.v1.component_groups import ComponentGroup # noqa 6 | from cachetclient.v1.incidents import Incident # noqa 7 | from cachetclient.v1.incident_updates import IncidentUpdate # noqa 8 | from cachetclient.v1.metrics import Metric # noqa 9 | from cachetclient.v1.metric_points import MetricPoint # noqa 10 | from cachetclient.v1.schedules import Schedule # noqa 11 | from cachetclient.v1 import enums # noqa 12 | -------------------------------------------------------------------------------- /tests/test_ping.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from base import CachetTestcase 4 | from fakeapi import FakeHttpClient 5 | 6 | 7 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 8 | class PingTests(CachetTestcase): 9 | 10 | def test_ping_call(self): 11 | """Ping using __call__ method""" 12 | client = self.create_client() 13 | result = client.ping() 14 | self.assertTrue(result) 15 | 16 | def test_ping_get(self): 17 | """Ping using get method""" 18 | client = self.create_client() 19 | result = client.ping.get() 20 | self.assertTrue(result) 21 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. cachet-client documentation master file, created by 2 | sphinx-quickstart on Fri Jun 21 17:12:21 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to cachet-client's documentation! 7 | ========================================= 8 | 9 | cachet-client is a python 3.5+ client library for the open source status 10 | page system `Cachet `_ . 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | guide/index 16 | reference/index 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Extras 3 | 4 | Contains simple live testing and release instructions. 5 | 6 | ## Live Tests 7 | 8 | This is a simple module doing some quick sanity checking using 9 | and actual cachet service. 10 | 11 | * Start up cachet using the docker-compose setup in the project root 12 | * Should have cachet 2.3 and 2.4 with separate databases 13 | * You may need to commend out the API_TOKEN on first run and 14 | fetch the generated one in your local cachet service on first startup. 15 | * Edit `env23.sh` and `env24.sh` with your own API tokens 16 | * Run `live_run.py` with each environment. 17 | 18 | Environment variables for `live_run.py`: 19 | 20 | ```bash 21 | CACHET_ENDPOINT 22 | CACHET_API_TOKEN 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.version.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.version 2 | .. py:currentmodule:: cachetclient.v1.version 3 | 4 | Version 5 | ======= 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Version.__init__ 14 | .. automethod:: Version.get 15 | 16 | Attributes 17 | ********** 18 | 19 | .. autoattribute:: Version.attrs 20 | .. autoattribute:: Version.value 21 | .. autoattribute:: Version.on_latest 22 | .. autoattribute:: Version.latest 23 | 24 | Manager 25 | ------- 26 | 27 | Methods 28 | ******* 29 | 30 | .. automethod:: VersionManager.__init__ 31 | .. automethod:: VersionManager.get 32 | .. automethod:: VersionManager.__call__ 33 | .. automethod:: VersionManager.instance_list_from_json 34 | .. automethod:: VersionManager.instance_from_dict 35 | .. automethod:: VersionManager.instance_from_json 36 | 37 | Attributes 38 | ********** 39 | 40 | .. autoattribute:: VersionManager.path 41 | .. autoattribute:: VersionManager.resource_class 42 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from base import CachetTestcase 4 | from fakeapi import FakeHttpClient 5 | 6 | 7 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 8 | class VersionTests(CachetTestcase): 9 | 10 | def test_version_call(self): 11 | """Version using __call__ method""" 12 | client = self.create_client() 13 | self.check_result(client.version()) 14 | 15 | def test_version_get(self): 16 | """Version using get method""" 17 | client = self.create_client() 18 | self.check_result(client.version.get()) 19 | 20 | def check_result(self, version): 21 | """Test version resource values""" 22 | self.assertEqual(version.value, "2.3.11-dev") 23 | self.assertEqual(version.on_latest, True) 24 | self.assertEqual( 25 | version.latest, 26 | { 27 | "tag_name": "v2.3.10", 28 | "prelease": False, 29 | "draft": False, 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | # Cachet 2.3.x 4 | cachet23: 5 | image: cachethq/docker:2.3.15 6 | env_file: 7 | - cachet23.env 8 | ports: 9 | - "8000:8000" 10 | depends_on: 11 | - cachet23_db 12 | cachet23_db: 13 | image: mariadb:10.5 14 | volumes: 15 | - db23:/var/lib/mysql 16 | environment: 17 | - MYSQL_ROOT_PASSWORD=root 18 | - MYSQL_DATABASE=status 19 | - MYSQL_USER=test 20 | - MYSQL_PASSWORD=test 21 | 22 | # Cachet 2.4.x 23 | # Assumes you cloned https://github.com/CachetHQ/Docker 24 | cachet24: 25 | build: 26 | context: ../Docker 27 | env_file: 28 | - cachet24.env 29 | ports: 30 | - "8080:8000" 31 | depends_on: 32 | - cachet24_db 33 | cachet24_db: 34 | image: mariadb:10.5 35 | volumes: 36 | - db24:/var/lib/mysql 37 | environment: 38 | - MYSQL_ROOT_PASSWORD=root 39 | - MYSQL_DATABASE=status 40 | - MYSQL_USER=test 41 | - MYSQL_PASSWORD=test 42 | 43 | 44 | volumes: 45 | db23: 46 | db24: 47 | -------------------------------------------------------------------------------- /cachetclient/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import re 3 | from datetime import datetime 4 | 5 | 6 | def to_datetime(timestamp: Optional[str]) -> Optional[datetime]: 7 | """ 8 | Convert string to datetime of formats:: 9 | 10 | '2019-05-24 09:26:22' 11 | 'Friday 24th May 2019 10:01:44' 12 | 13 | Args: 14 | timestamp (str): String timestamp 15 | 16 | Returns: 17 | datetime if input is a valid datetime string 18 | """ 19 | if timestamp is None: 20 | return None 21 | 22 | try: 23 | # '2019-05-24 09:26:22' 24 | return datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") 25 | except ValueError: 26 | pass 27 | 28 | try: 29 | # 'Friday 1st May 2019 10:01:44'. Used in verified_at for subscribers 30 | sub_timestamp = re.sub(r"\b([0123]?[0-9])(st|th|nd|rd)\b", r"\1", timestamp) 31 | return datetime.strptime(sub_timestamp, "%A %d %B %Y %H:%M:%S") 32 | except ValueError: 33 | pass 34 | 35 | raise ValueError("datetime string '{}' not supported".format(timestamp)) 36 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: cachet-client 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8, 3.9] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | python -m pip install . 23 | - name: Lint with flake8 24 | run: | 25 | pip install flake8 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with pytest 31 | run: | 32 | pip install pytest 33 | pytest 34 | -------------------------------------------------------------------------------- /cachetclient/v1/ping.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from cachetclient.base import Manager 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class PingManager(Manager): 8 | """Manager for ping endpoints""" 9 | 10 | path = "ping" 11 | 12 | def __call__(self) -> bool: 13 | """ 14 | Shortcut for the :py:data:`get` method. 15 | 16 | Example:: 17 | 18 | >> client.ping() 19 | True 20 | """ 21 | return self.get() 22 | 23 | def get(self) -> bool: 24 | """ 25 | Check if the cachet api is responding. 26 | 27 | Example:: 28 | 29 | >> client.ping.get() 30 | True 31 | 32 | Returns: 33 | bool: ``True`` if a successful response. Otherwise ``False``. 34 | """ 35 | # FIXME: Test more explicit exceptions 36 | try: 37 | response = self._http.get(self.path) 38 | data = response.json() 39 | return data["data"] == "Pong!" 40 | except Exception as ex: 41 | logger.warning("Ping: %s", ex) 42 | return False 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zetta.IO Technology AS 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="cachet-client", 6 | version="4.0.1", 7 | description="A python 3 client for the Cachet API", 8 | long_description=open('README.md').read(), 9 | long_description_content_type='text/markdown', 10 | url="https://github.com/zettaio/cachet-client", 11 | author="Einar Forselv", 12 | author_email="eforselv@gmail.com", 13 | maintainer="Einar Forselv", 14 | maintainer_email="eforselv@gmail.com", 15 | packages=['cachetclient', 'cachetclient.v1'], 16 | include_package_data=True, 17 | keywords=['cachet', 'client', 'api'], 18 | python_requires='>=3.5', 19 | classifiers=[ 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Programming Language :: Python :: 3.9', 26 | 'License :: OSI Approved :: BSD License', 27 | ], 28 | install_requires=[ 29 | 'requests>=2.21.0' 30 | ], 31 | entry_points={'console_scripts': [ 32 | 'cachet = cachetclient.cli:execute_from_command_line', 33 | ]}, 34 | ) 35 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.schedules.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.schedules 2 | .. py:currentmodule:: cachetclient.v1.schedules 3 | 4 | Schedules 5 | ========= 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Schedule.get 14 | .. automethod:: Schedule.update 15 | .. automethod:: Schedule.delete 16 | 17 | Attributes 18 | ********** 19 | 20 | .. autoattribute:: Schedule.id 21 | .. autoattribute:: Schedule.name 22 | .. autoattribute:: Schedule.message 23 | .. autoattribute:: Schedule.status 24 | .. autoattribute:: Schedule.scheduled_at 25 | .. autoattribute:: Schedule.completed_at 26 | .. autoattribute:: Schedule.attrs 27 | 28 | Manager 29 | ------- 30 | 31 | Methods 32 | ******* 33 | 34 | .. automethod:: ScheduleManager.__init__ 35 | .. automethod:: ScheduleManager.create 36 | .. automethod:: ScheduleManager.update 37 | .. automethod:: ScheduleManager.list 38 | .. automethod:: ScheduleManager.get 39 | .. automethod:: ScheduleManager.delete 40 | .. automethod:: ScheduleManager.count 41 | .. automethod:: ScheduleManager.instance_from_dict 42 | .. automethod:: ScheduleManager.instance_from_json 43 | .. automethod:: ScheduleManager.instance_list_from_json 44 | 45 | Attributes 46 | ********** 47 | 48 | .. autoattribute:: ScheduleManager.path 49 | .. autoattribute:: ScheduleManager.resource_class 50 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.subscribers.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.subscribers 2 | .. py:currentmodule:: cachetclient.v1.subscribers 3 | 4 | Subscribers 5 | =========== 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Subscriber.__init__ 14 | .. automethod:: Subscriber.update 15 | .. automethod:: Subscriber.get 16 | .. automethod:: Subscriber.delete 17 | 18 | Attributes 19 | ********** 20 | 21 | .. autoattribute:: Subscriber.attrs 22 | .. autoattribute:: Subscriber.id 23 | .. autoattribute:: Subscriber.email 24 | .. autoattribute:: Subscriber.verify_code 25 | .. autoattribute:: Subscriber.is_global 26 | .. autoattribute:: Subscriber.created_at 27 | .. autoattribute:: Subscriber.updated_at 28 | .. autoattribute:: Subscriber.verified_at 29 | 30 | Manager 31 | ------- 32 | 33 | Methods 34 | ******* 35 | 36 | .. automethod:: SubscriberManager.__init__ 37 | .. automethod:: SubscriberManager.create 38 | .. automethod:: SubscriberManager.list 39 | .. automethod:: SubscriberManager.delete 40 | .. automethod:: SubscriberManager.count 41 | .. automethod:: SubscriberManager.instance_from_dict 42 | .. automethod:: SubscriberManager.instance_from_json 43 | .. automethod:: SubscriberManager.instance_list_from_json 44 | 45 | Attributes 46 | ********** 47 | 48 | .. autoattribute:: SubscriberManager.path 49 | .. autoattribute:: SubscriberManager.resource_class 50 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.metric_points.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.metric_points 2 | .. py:currentmodule:: cachetclient.v1.metric_points 3 | 4 | Metric Points 5 | ============= 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: MetricPoint.__init__ 14 | .. automethod:: MetricPoint.get 15 | .. automethod:: MetricPoint.delete 16 | .. automethod:: MetricPoint.update 17 | 18 | Attributes 19 | ********** 20 | 21 | .. autoattribute:: MetricPoint.attrs 22 | .. autoattribute:: MetricPoint.metric_id 23 | .. autoattribute:: MetricPoint.value 24 | .. autoattribute:: MetricPoint.created_at 25 | .. autoattribute:: MetricPoint.id 26 | .. autoattribute:: MetricPoint.updated_at 27 | .. autoattribute:: MetricPoint.calculated_value 28 | .. autoattribute:: MetricPoint.counter 29 | 30 | Manager 31 | ------- 32 | 33 | Methods 34 | ******* 35 | 36 | .. automethod:: MetricPointsManager.__init__ 37 | .. automethod:: MetricPointsManager.create 38 | .. automethod:: MetricPointsManager.list 39 | .. automethod:: MetricPointsManager.count 40 | .. automethod:: MetricPointsManager.delete 41 | .. automethod:: MetricPointsManager.instance_from_dict 42 | .. automethod:: MetricPointsManager.instance_from_json 43 | .. automethod:: MetricPointsManager.instance_list_from_json 44 | 45 | Attributes 46 | ********** 47 | 48 | .. autoattribute:: MetricPointsManager.path 49 | .. autoattribute:: MetricPointsManager.resource_class 50 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.enums.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.enums 2 | .. py:currentmodule:: cachetclient.v1.enums 3 | 4 | enums 5 | ===== 6 | 7 | Constants / enums for various resources in cachet 8 | like component and incident status value. 9 | 10 | Component Status 11 | ---------------- 12 | 13 | .. autodata:: COMPONENT_STATUS_OPERATIONAL 14 | :annotation: 15 | .. autodata:: COMPONENT_STATUS_PERFORMANCE_ISSUES 16 | :annotation: 17 | .. autodata:: COMPONENT_STATUS_PARTIAL_OUTAGE 18 | :annotation: 19 | .. autodata:: COMPONENT_STATUS_MAJOR_OUTAGE 20 | :annotation: 21 | .. autodata:: COMPONENT_STATUS_LIST 22 | :annotation: 23 | 24 | Component Group Collapsed 25 | ------------------------- 26 | 27 | .. autodata:: COMPONENT_GROUP_COLLAPSED_FALSE 28 | :annotation: 29 | .. autodata:: COMPONENT_GROUP_COLLAPSED_TRUE 30 | :annotation: 31 | .. autodata:: COMPONENT_GROUP_COLLAPSED_NOT_OPERATIONAL 32 | :annotation: 33 | 34 | Incident Status 35 | --------------- 36 | 37 | .. autodata:: INCIDENT_SCHEDULED 38 | :annotation: 39 | .. autodata:: INCIDENT_INVESTIGATING 40 | :annotation: 41 | .. autodata:: INCIDENT_IDENTIFIED 42 | :annotation: 43 | .. autodata:: INCIDENT_WATCHING 44 | :annotation: 45 | .. autodata:: INCIDENT_FIXED 46 | :annotation: 47 | .. autofunction:: incident_status_human 48 | 49 | Schedule Status 50 | --------------- 51 | 52 | .. autodata:: SCHEDULE_STATUS_UPCOMING 53 | :annotation: 54 | .. autodata:: SCHEDULE_STATUS_IN_PROGRESS 55 | :annotation: 56 | .. autodata:: SCHEDULE_STATUS_COMPLETE 57 | :annotation: 58 | -------------------------------------------------------------------------------- /cachetclient/v1/client.py: -------------------------------------------------------------------------------- 1 | from cachetclient.httpclient import HttpClient 2 | from cachetclient.v1.component_groups import ComponentGroupManager 3 | from cachetclient.v1.components import ComponentManager 4 | from cachetclient.v1.incidents import IncidentManager 5 | from cachetclient.v1.incident_updates import IncidentUpdatesManager 6 | from cachetclient.v1.metrics import MetricsManager 7 | from cachetclient.v1.metric_points import MetricPointsManager 8 | from cachetclient.v1.subscribers import SubscriberManager 9 | from cachetclient.v1.ping import PingManager 10 | from cachetclient.v1.version import VersionManager 11 | from cachetclient.v1.schedules import ScheduleManager 12 | 13 | 14 | class Client: 15 | def __init__(self, http_client: HttpClient): 16 | """ 17 | Args: 18 | http_client: The http client class to use 19 | """ 20 | self._http = http_client 21 | 22 | # Managers 23 | self.ping = PingManager(self._http) 24 | self.version = VersionManager(self._http) 25 | self.components = ComponentManager(self._http) 26 | self.component_groups = ComponentGroupManager(self._http, self.components) 27 | self.incident_updates = IncidentUpdatesManager(self._http) 28 | self.incidents = IncidentManager(self._http, self.incident_updates) 29 | self.metric_points = MetricPointsManager(self._http) 30 | self.metrics = MetricsManager(self._http, self.metric_points) 31 | self.subscribers = SubscriberManager(self._http) 32 | self.schedules = ScheduleManager(self._http) 33 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.metrics.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.metrics 2 | .. py:currentmodule:: cachetclient.v1.metrics 3 | 4 | Metrics 5 | ======= 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Metric.__init__ 14 | .. automethod:: Metric.get 15 | .. automethod:: Metric.delete 16 | .. automethod:: Metric.update 17 | 18 | Attributes 19 | ********** 20 | 21 | .. autoattribute:: Metric.attrs 22 | .. autoattribute:: Metric.id 23 | .. autoattribute:: Metric.name 24 | .. autoattribute:: Metric.description 25 | .. autoattribute:: Metric.default_value 26 | .. autoattribute:: Metric.display_chart 27 | .. autoattribute:: Metric.places 28 | .. autoattribute:: Metric.points 29 | .. autoattribute:: Metric.threshold 30 | .. autoattribute:: Metric.visible 31 | .. autoattribute:: Metric.order 32 | .. autoattribute:: Metric.suffix 33 | .. autoattribute:: Metric.calc_type 34 | .. autoattribute:: Metric.default_view 35 | .. autoattribute:: Metric.created_at 36 | .. autoattribute:: Metric.updated_at 37 | 38 | Manager 39 | ------- 40 | 41 | Methods 42 | ******* 43 | 44 | .. automethod:: MetricsManager.__init__ 45 | .. automethod:: MetricsManager.create 46 | .. automethod:: MetricsManager.get 47 | .. automethod:: MetricsManager.list 48 | .. automethod:: MetricsManager.count 49 | .. automethod:: MetricsManager.delete 50 | .. automethod:: MetricsManager.instance_from_dict 51 | .. automethod:: MetricsManager.instance_from_json 52 | .. automethod:: MetricsManager.instance_list_from_json 53 | 54 | Attributes 55 | ********** 56 | 57 | .. autoattribute:: MetricsManager.path 58 | .. autoattribute:: MetricsManager.resource_class 59 | -------------------------------------------------------------------------------- /cachetclient/v1/version.py: -------------------------------------------------------------------------------- 1 | from cachetclient.base import Manager, Resource 2 | 3 | 4 | class Version(Resource): 5 | @property 6 | def value(self) -> str: 7 | """str: Version string from Cachet service""" 8 | return self._data["data"] 9 | 10 | @property 11 | def on_latest(self) -> bool: 12 | """bool: Are we on latest version? 13 | Requires beacon enabled on server. 14 | """ 15 | return self._data["meta"]["on_latest"] 16 | 17 | @property 18 | def latest(self) -> dict: 19 | """dict: Obtains info dict about latest version. 20 | Requires beacon enabled on server. 21 | 22 | Dict format is:: 23 | 24 | { 25 | "tag_name": "v2.3.10", 26 | "prelease": false, 27 | "draft": false 28 | } 29 | 30 | """ 31 | return self._data["meta"]["latest"] 32 | 33 | 34 | class VersionManager(Manager): 35 | resource_class = Version 36 | path = "version" 37 | 38 | def __call__(self) -> Version: 39 | """Shortcut to :py:data:`get` 40 | 41 | Example:: 42 | 43 | >> version = client.version() 44 | >> version.value 45 | v2.3.10 46 | """ 47 | return self.get() 48 | 49 | def get(self) -> Version: 50 | """Get version info from the server 51 | 52 | Example:: 53 | 54 | >> version = client.version.get() 55 | >> version.value 56 | v2.3.10 57 | 58 | Returns: 59 | :py:data:`Version` instance 60 | """ 61 | response = self._http.get(self.path) 62 | return Version(self, response.json()) 63 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.incident_updates.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.incident_updates 2 | .. py:currentmodule:: cachetclient.v1.incident_updates 3 | 4 | IncidentUpdates 5 | =============== 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: IncidentUpdate.__init__ 14 | .. automethod:: IncidentUpdate.update 15 | .. automethod:: IncidentUpdate.get 16 | .. automethod:: IncidentUpdate.delete 17 | 18 | Attributes 19 | ********** 20 | 21 | .. autoattribute:: IncidentUpdate.attrs 22 | .. autoattribute:: IncidentUpdate.id 23 | .. autoattribute:: IncidentUpdate.incident_id 24 | .. autoattribute:: IncidentUpdate.status 25 | .. autoattribute:: IncidentUpdate.message 26 | .. autoattribute:: IncidentUpdate.user_id 27 | .. autoattribute:: IncidentUpdate.created_at 28 | .. autoattribute:: IncidentUpdate.updated_at 29 | .. autoattribute:: IncidentUpdate.human_status 30 | .. autoattribute:: IncidentUpdate.permalink 31 | 32 | Manager 33 | ------- 34 | 35 | Methods 36 | ******* 37 | 38 | .. automethod:: IncidentUpdatesManager.__init__ 39 | .. automethod:: IncidentUpdatesManager.create 40 | .. automethod:: IncidentUpdatesManager.update 41 | .. automethod:: IncidentUpdatesManager.count 42 | .. automethod:: IncidentUpdatesManager.list 43 | .. automethod:: IncidentUpdatesManager.get 44 | .. automethod:: IncidentUpdatesManager.delete 45 | .. automethod:: IncidentUpdatesManager.instance_from_dict 46 | .. automethod:: IncidentUpdatesManager.instance_from_json 47 | .. automethod:: IncidentUpdatesManager.instance_list_from_json 48 | 49 | Attributes 50 | ********** 51 | 52 | .. autoattribute:: IncidentUpdatesManager.path 53 | .. autoattribute:: IncidentUpdatesManager.resource_class 54 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.incidents.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.incidents 2 | .. py:currentmodule:: cachetclient.v1.incidents 3 | 4 | Incidents 5 | ========= 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Incident.__init__ 14 | .. automethod:: Incident.updates 15 | .. automethod:: Incident.update 16 | .. automethod:: Incident.get 17 | .. automethod:: Incident.delete 18 | 19 | Attributes 20 | ********** 21 | 22 | .. autoattribute:: Incident.attrs 23 | .. autoattribute:: Incident.id 24 | .. autoattribute:: Incident.component_id 25 | .. autoattribute:: Incident.name 26 | .. autoattribute:: Incident.message 27 | .. autoattribute:: Incident.notify 28 | .. autoattribute:: Incident.status 29 | .. autoattribute:: Incident.human_status 30 | .. autoattribute:: Incident.visible 31 | .. autoattribute:: Incident.stickied 32 | .. autoattribute:: Incident.scheduled_at 33 | .. autoattribute:: Incident.created_at 34 | .. autoattribute:: Incident.occurred_at 35 | .. autoattribute:: Incident.updated_at 36 | .. autoattribute:: Incident.deleted_at 37 | 38 | Manager 39 | ------- 40 | 41 | Methods 42 | ******* 43 | 44 | .. automethod:: IncidentManager.__init__ 45 | .. automethod:: IncidentManager.create 46 | .. automethod:: IncidentManager.update 47 | .. automethod:: IncidentManager.list 48 | .. automethod:: IncidentManager.get 49 | .. automethod:: IncidentManager.count 50 | .. automethod:: IncidentManager.delete 51 | .. automethod:: IncidentManager.instance_from_dict 52 | .. automethod:: IncidentManager.instance_from_json 53 | .. automethod:: IncidentManager.instance_list_from_json 54 | 55 | Attributes 56 | ********** 57 | 58 | .. autoattribute:: IncidentManager.path 59 | .. autoattribute:: IncidentManager.resource_class 60 | -------------------------------------------------------------------------------- /docs/guide/install.rst: -------------------------------------------------------------------------------- 1 | 2 | Install 3 | ======= 4 | 5 | A package is available on PyPI:: 6 | 7 | pip install cachet-client 8 | 9 | Building from source:: 10 | 11 | git clone https://github.com/ZettaIO/cachet-client.git (or use ssh) 12 | python setup.py bdist_wheel 13 | # .whl will be located in dist/ directory and can be installed later with pip 14 | 15 | Development Setup 16 | ----------------- 17 | 18 | Development install:: 19 | 20 | git clone https://github.com/ZettaIO/cachet-client.git (or use ssh) 21 | cd cachet-client 22 | python -m virtualenv .venv 23 | . .venv/bin/activate 24 | pip install -e . 25 | 26 | Building docs:: 27 | 28 | pip install -r docs/requirements.txt 29 | python setup.py build_sphinx 30 | 31 | Running unit tests:: 32 | 33 | pip install -r tests/requirements.txt 34 | tox 35 | 36 | # Optionally 37 | tox -e py36 # tests only 38 | tox -e pep8 # for pep8 run only 39 | 40 | # Running tests with pytest also works, but this works poorly in combination 41 | # with environment variables for the live test script (tox separates environments) 42 | pytest tests/ 43 | 44 | Testing with real Cachet service 45 | -------------------------------- 46 | 47 | Do not run this script against a system in production. 48 | This is only for a test service. Cachet can easily be set up locally 49 | with docker: https://github.com/CachetHQ/Docker 50 | 51 | You need to set the following environment variables:: 52 | 53 | CACHET_ENDPOINT 54 | CACHET_API_TOKEN 55 | 56 | Running tests:: 57 | 58 | python extras/live_run.py 59 | ... 60 | ================================================= 61 | Numer of tests : 10 62 | Succesful : 10 63 | Failure : 0 64 | Percentage passed : 100.0% 65 | ================================================= 66 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.component_groups.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.component_groups 2 | .. py:currentmodule:: cachetclient.v1.component_groups 3 | 4 | Component Groups 5 | ================ 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: ComponentGroup.__init__ 14 | .. automethod:: ComponentGroup.update 15 | .. automethod:: ComponentGroup.get 16 | .. automethod:: ComponentGroup.delete 17 | 18 | Attributes 19 | ********** 20 | 21 | .. autoattribute:: ComponentGroup.attrs 22 | .. autoattribute:: ComponentGroup.id 23 | .. autoattribute:: ComponentGroup.name 24 | .. autoattribute:: ComponentGroup.enabled_components 25 | .. autoattribute:: ComponentGroup.order 26 | .. autoattribute:: ComponentGroup.collapsed 27 | .. autoattribute:: ComponentGroup.lowest_human_status 28 | .. autoattribute:: ComponentGroup.is_collapsed 29 | .. autoattribute:: ComponentGroup.is_open 30 | .. autoattribute:: ComponentGroup.is_operational 31 | .. autoattribute:: ComponentGroup.created_at 32 | .. autoattribute:: ComponentGroup.updated_at 33 | .. autoattribute:: ComponentGroup.visible 34 | 35 | Manager 36 | ------- 37 | 38 | Methods 39 | ******* 40 | 41 | .. automethod:: ComponentGroupManager.__init__ 42 | .. automethod:: ComponentGroupManager.create 43 | .. automethod:: ComponentGroupManager.update 44 | .. automethod:: ComponentGroupManager.count 45 | .. automethod:: ComponentGroupManager.list 46 | .. automethod:: ComponentGroupManager.get 47 | .. automethod:: ComponentGroupManager.delete 48 | .. automethod:: ComponentGroupManager.instance_from_dict 49 | .. automethod:: ComponentGroupManager.instance_from_json 50 | .. automethod:: ComponentGroupManager.instance_list_from_json 51 | 52 | Attributes 53 | ********** 54 | 55 | .. autoattribute:: ComponentGroupManager.resource_class 56 | .. autoattribute:: ComponentGroupManager.path 57 | -------------------------------------------------------------------------------- /docs/reference/cachetclient.v1.components.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: cachetclient.v1.components 2 | .. py:currentmodule:: cachetclient.v1.components 3 | 4 | Components 5 | ========== 6 | 7 | Resource 8 | -------- 9 | 10 | Methods 11 | ******* 12 | 13 | .. automethod:: Component.__init__ 14 | .. automethod:: Component.add_tag 15 | .. automethod:: Component.add_tags 16 | .. automethod:: Component.set_tags 17 | .. automethod:: Component.del_tag 18 | .. automethod:: Component.has_tag 19 | .. automethod:: Component.update 20 | .. automethod:: Component.get 21 | .. automethod:: Component.delete 22 | 23 | Attributes 24 | ********** 25 | 26 | .. autoattribute:: Component.attrs 27 | .. autoattribute:: Component.id 28 | .. autoattribute:: Component.name 29 | .. autoattribute:: Component.description 30 | .. autoattribute:: Component.link 31 | .. autoattribute:: Component.status 32 | .. autoattribute:: Component.status_name 33 | .. autoattribute:: Component.order 34 | .. autoattribute:: Component.group_id 35 | .. autoattribute:: Component.enabled 36 | .. autoattribute:: Component.tags 37 | .. autoattribute:: Component.tag_names 38 | .. autoattribute:: Component.tag_slugs 39 | .. autoattribute:: Component.created_at 40 | .. autoattribute:: Component.updated_at 41 | 42 | Manager 43 | ------- 44 | 45 | Methods 46 | ******* 47 | 48 | .. automethod:: ComponentManager.__init__ 49 | .. automethod:: ComponentManager.create 50 | .. automethod:: ComponentManager.update 51 | .. automethod:: ComponentManager.list 52 | .. automethod:: ComponentManager.get 53 | .. automethod:: ComponentManager.count 54 | .. automethod:: ComponentManager.delete 55 | .. automethod:: ComponentManager.instance_from_dict 56 | .. automethod:: ComponentManager.instance_from_json 57 | .. automethod:: ComponentManager.instance_list_from_json 58 | 59 | Attributes 60 | ********** 61 | 62 | .. autoattribute:: ComponentManager.path 63 | .. autoattribute:: ComponentManager.resource_class 64 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Ensure that this file do not contain non-ascii characters 2 | # as flake8 can fail to parse the file on OS X and Windows 3 | 4 | [tox] 5 | envlist = 6 | pep8 7 | py36 8 | py37 9 | py38 10 | py39 11 | 12 | [testenv:pep8] 13 | usedevelop = false 14 | deps = flake8 15 | basepython = python3.7 16 | commands = flake8 17 | 18 | [testenv] 19 | usedevelop = True 20 | basepython = 21 | py35: python3.5 22 | py36: python3.6 23 | py37: python3.7 24 | py38: python3.8 25 | py39: python3.9 26 | deps = -r{toxinidir}/tests/requirements.txt 27 | commands = 28 | pytest tests/ 29 | 30 | [pytest] 31 | norecursedirs = .venv/* .tox/* 32 | 33 | [flake8] 34 | # H405: multi line docstring summary not separated with an empty line 35 | # D100: Missing docstring in public module 36 | # D101: Missing docstring in public class 37 | # D102: Missing docstring in public method 38 | # D103: Missing docstring in public function 39 | # D104: Missing docstring in public package 40 | # D105: Missing docstring in magic method 41 | # D200: One-line docstring should fit on one line with quotes 42 | # D202: No blank lines allowed after function docstring 43 | # D203: 1 blank required before class docstring. 44 | # D204: 1 blank required after class docstring 45 | # D205: Blank line required between one-line summary and description. 46 | # D207: Docstring is under-indented 47 | # D208: Docstring is over-indented 48 | # D211: No blank lines allowed before class docstring 49 | # D301: Use r""" if any backslashes in a docstring 50 | # D400: First line should end with a period. 51 | # D401: First line should be in imperative mood. 52 | # *** E302 expected 2 blank lines, found 1 53 | # *** W503 line break before binary operator 54 | ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D207,D208,D211,D301,D400,D401,W503 55 | show-source = True 56 | max-line-length = 120 57 | exclude = .tox,.venv,setup.py,docs,tests 58 | -------------------------------------------------------------------------------- /tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import mock 3 | from requests.exceptions import HTTPError 4 | 5 | from base import CachetTestcase 6 | from fakeapi import FakeHttpClient 7 | from cachetclient.v1 import enums 8 | 9 | 10 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 11 | class MetricsTests(CachetTestcase): 12 | 13 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 14 | def setUp(self): 15 | self.client = self.create_client() 16 | 17 | def test_get(self): 18 | first = self.client.metrics.create(name="Issue 1", description="Descr", suffix='IS') 19 | print(first.__dict__) 20 | self.client.metrics.create(name="Issue 2", description="Descr", suffix='IS2') 21 | self.client.metrics.create(name="Issue 3", description="Descr", suffix='IS3') 22 | 23 | self.assertEqual(self.client.metrics.count(), 3) 24 | 25 | metrics = self.client.metrics.list() 26 | metrics = list(metrics) 27 | self.assertEqual(len(metrics), 3) 28 | 29 | # Re-fetch a single metric 30 | metric = self.client.metrics.get(first.id) 31 | self.assertEqual(first.id, metric.id) 32 | 33 | def test_create(self): 34 | metric = self.client.metrics.create( 35 | name="Something blew up!", 36 | description="We are looking into it", 37 | suffix="SO" 38 | ) 39 | 40 | self.assertEqual(metric.id, 1) 41 | self.assertEqual(metric.name, "Something blew up!") 42 | self.assertEqual(metric.description, "We are looking into it") 43 | self.assertEqual(metric.suffix, "SO") 44 | self.assertEqual(metric.default_value, 0) 45 | self.assertEqual(metric.display_chart, 0) 46 | self.assertIsInstance(metric.created_at, datetime) 47 | self.assertIsInstance(metric.updated_at, datetime) 48 | 49 | metric.delete() 50 | -------------------------------------------------------------------------------- /tests/test_metric_points.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from datetime import datetime 3 | 4 | from base import CachetTestcase 5 | from fakeapi import FakeHttpClient 6 | from cachetclient.v1 import enums 7 | 8 | 9 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 10 | class MetricPointsTest(CachetTestcase): 11 | 12 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 13 | def setUp(self): 14 | self.client = self.create_client() 15 | 16 | def test_create(self): 17 | metric = self.client.metrics.create( 18 | name="Issue 1", 19 | description="Descr", 20 | suffix='IS', 21 | default_value=0 22 | ) 23 | 24 | # Add 3 updates 25 | first = self.client.metric_points.create( 26 | metric_id=metric.id, 27 | value=1, 28 | ) 29 | # Test all properties 30 | self.assertEqual(first.id, 1) 31 | self.assertEqual(first.metric_id, 1) 32 | self.assertEqual(first.value, 1) 33 | self.assertIsInstance(first.created_at, datetime) 34 | self.assertIsInstance(first.updated_at, datetime) 35 | 36 | # Add to check_list for later testing 37 | check_list = [{'id': first.id, 'metric_id': 1, 'value': first.value}] 38 | 39 | # create a couple of entries 40 | for point in range(1, 3): 41 | self.client.metric_points.create( 42 | metric_id=metric.id, 43 | value=point+1, 44 | ) 45 | # Append to check_list for later testing 46 | check_list.append({'id': point+1, 'metric_id': 1, 'value': point+1}) 47 | 48 | self.assertEqual(self.client.metric_points.count(metric.id), 3) 49 | 50 | # List and compare 51 | points = list(metric.points()) 52 | self.assertEqual(len(points), 3) 53 | self.assertEqual( 54 | [{k: i.attrs[k] for k in ['id', 'metric_id', 'value']} for i in points], 55 | check_list 56 | ) 57 | -------------------------------------------------------------------------------- /tests/test_schedules.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from requests.exceptions import HTTPError 4 | from unittest import mock 5 | from datetime import datetime 6 | 7 | from base import CachetTestcase 8 | from fakeapi import FakeHttpClient 9 | from cachetclient.v1 import enums 10 | 11 | 12 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 13 | class SchedulerTests(CachetTestcase): 14 | 15 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 16 | def setUp(self): 17 | self.client = self.create_client() 18 | 19 | def test_create(self): 20 | start_time = datetime.strptime('2020-09-07 00:18:00', '%Y-%m-%d %H:%M:%S') 21 | instance = self.client.schedules.create( 22 | name="Planned Maintenance", 23 | status=enums.SCHEDULE_STATUS_UPCOMING, 24 | message="We're doing some maintenance today", 25 | scheduled_at=start_time, 26 | notify=False, 27 | ) 28 | 29 | self.assertEqual(instance.id, 1) 30 | self.assertEqual(instance.status, enums.SCHEDULE_STATUS_UPCOMING) 31 | self.assertEqual(instance.name, "Planned Maintenance") 32 | self.assertEqual(instance.message, "We're doing some maintenance today") 33 | self.assertEqual(instance.scheduled_at, start_time) 34 | self.assertEqual(instance.completed_at, None) 35 | 36 | instance.delete() 37 | 38 | def test_list(self): 39 | start_time = datetime.strptime('2020-09-17 16:00', '%Y-%m-%d %H:%M') 40 | end_time = datetime.strptime('2020-09-17 18:00', '%Y-%m-%d %H:%M') 41 | for i in range(20): 42 | self.client.schedules.create( 43 | name="Planned Maintenance", 44 | status=enums.SCHEDULE_STATUS_UPCOMING, 45 | message="We're doing some maintenance today", 46 | scheduled_at=start_time, 47 | completed_at=end_time, 48 | ) 49 | 50 | self.assertEqual(self.client.schedules.count(), 20) 51 | self.assertIsInstance(self.client.schedules.list(), types.GeneratorType) 52 | instance = next(self.client.schedules.list(page=2, per_page=10)) 53 | self.assertEqual(instance.id, 11) 54 | -------------------------------------------------------------------------------- /docs/guide/basic_usage.rst: -------------------------------------------------------------------------------- 1 | 2 | Basic Usage 3 | =========== 4 | 5 | Creating a client 6 | ----------------- 7 | 8 | .. code:: python 9 | 10 | import cachetclient 11 | 12 | client = cachetclient.Client( 13 | endpoint='https://status.test/api/v1', 14 | api_token='secrettoken', 15 | ) 16 | 17 | Add a new subscriber with email verification 18 | -------------------------------------------- 19 | 20 | .. code:: python 21 | 22 | sub = client.subscribers.create(email='user@example.test', verify=False) 23 | 24 | List subscribers paginated 25 | -------------------------- 26 | 27 | .. code:: python 28 | 29 | # Pagination under the hood scaling better with large numbers of subscribers 30 | for sub in client.subscribers.list(page=1, per_page=100): 31 | print(sub.id, sub.email, sub.verify_code) 32 | 33 | Creating a component issue 34 | -------------------------- 35 | 36 | .. code:: python 37 | 38 | from cachetclient.v1 import enums 39 | 40 | # Issue signaling to a component there is a major outage 41 | client.incidents.create( 42 | name="Something blew up!", 43 | message="We are looking into it", 44 | status=enums.INCIDENT_INVESTIGATING, 45 | component_id=1, 46 | component_status=enums.COMPONENT_STATUS_MAJOR_OUTAGE, 47 | ) 48 | 49 | Creating component group with components 50 | ---------------------------------------- 51 | 52 | .. code:: python 53 | 54 | from cachetclient.v1 import enums 55 | 56 | group = client.component_groups.create(name="Global Services") 57 | component = client.components.create( 58 | name="Public webside", 59 | status=enums.COMPONENT_STATUS_OPERATIONAL, 60 | description="This is a test", 61 | tags="test, web, something", 62 | group_id=group.id, 63 | ) 64 | 65 | Recreating resource from json or dict 66 | ------------------------------------- 67 | 68 | Every manager has a method for recreating a resource 69 | instance from a json string or dictionary. This can be 70 | useful if data from cachet is cached or stored somewhere 71 | like memcache or a database. 72 | 73 | .. code:: python 74 | 75 | subscriber = client.subscribers.instance_from_json(json_str) 76 | subscriber = client.subscribers.instance_from_dict(data_dict) 77 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from unittest import mock, TestCase 2 | 3 | import cachetclient 4 | from base import CachetTestcase 5 | from fakeapi import FakeHttpClient 6 | 7 | 8 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 9 | class ClientTests(CachetTestcase): 10 | 11 | def test_mock(self): 12 | client = cachetclient.Client(endpoint=self.endpoint, api_token=self.token) 13 | self.assertTrue(client._http.is_fake_client) 14 | 15 | def test_basic(self): 16 | """Create a basic client""" 17 | cachetclient.Client(endpoint=self.endpoint, api_token=self.token) 18 | 19 | def test_endpoint_no_version(self): 20 | """Supply an endpoint without version""" 21 | with self.assertRaises(ValueError): 22 | cachetclient.Client(endpoint="meh", api_token=self.token) 23 | 24 | def test_endpoint_supply_version(self): 25 | """Test supplying modifier url through proxy etc""" 26 | cachetclient.Client(endpoint="https://status", version='1', api_token=self.token) 27 | 28 | def test_enviroment_vars(self): 29 | """Instantiate client using env vars""" 30 | envs = {'CACHET_API_TOKEN': self.token, 'CACHET_ENDPOINT': self.endpoint} 31 | with mock.patch.dict('os.environ', envs): 32 | cachetclient.Client() 33 | 34 | def test_missing_token(self): 35 | """Missing token raises error""" 36 | with self.assertRaises(ValueError): 37 | cachetclient.Client(endpoint=self.endpoint) 38 | 39 | def test_missing_endpoint(self): 40 | """Missing endpoint raises error""" 41 | with self.assertRaises(ValueError): 42 | cachetclient.Client(api_token=self.token) 43 | 44 | def test_missing_endpoint_env(self): 45 | """Missing endpoint env var should raise error""" 46 | envs = {'CACHET_API_TOKEN': self.token} 47 | with mock.patch.dict('os.environ', envs): 48 | with self.assertRaises(ValueError): 49 | cachetclient.Client() 50 | 51 | def test_missing_token_env(self): 52 | """Missing token env var should raise error""" 53 | envs = {'CACHET_ENDPOINT': self.endpoint} 54 | with mock.patch.dict('os.environ', envs): 55 | with self.assertRaises(ValueError): 56 | cachetclient.Client() 57 | -------------------------------------------------------------------------------- /cachetclient/httpclient.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | Dict, 4 | ) 5 | import logging 6 | from urllib.parse import urljoin 7 | 8 | import requests 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class HttpClient: 14 | def __init__( 15 | self, 16 | base_url: str, 17 | api_token: str, 18 | timeout: float = None, 19 | verify_tls: bool = True, 20 | user_agent: str = None, 21 | ): 22 | 23 | self.base_url = base_url 24 | if not self.base_url.endswith("/"): 25 | self.base_url += "/" 26 | self.verify_tls = verify_tls 27 | self.timeout = timeout 28 | self.user_agent = user_agent 29 | 30 | self._session = requests.Session() 31 | self._session.headers.update( 32 | { 33 | "X-Cachet-Token": api_token, 34 | "Accept": "application/json", 35 | "Content-Type": "application/json", 36 | } 37 | ) 38 | if user_agent: 39 | self._session.headers.update({"User-Agent": user_agent}) 40 | 41 | def get(self, path, params=None) -> requests.Response: 42 | return self.request("GET", path, params=params) 43 | 44 | def post(self, path, data) -> requests.Response: 45 | return self.request("POST", path, data=data) 46 | 47 | def put(self, path, data) -> requests.Response: 48 | return self.request("PUT", path, data=data) 49 | 50 | def delete(self, path, resource_id) -> requests.Response: 51 | return self.request("DELETE", "{}/{}".format(path, resource_id)) 52 | 53 | def request( 54 | self, 55 | method: str, 56 | path: str, 57 | params: Dict[str, Any] = None, 58 | data: Dict[str, Any] = None, 59 | ) -> requests.Response: 60 | url = urljoin(self.base_url, path) 61 | response = self._session.request( 62 | method, 63 | url, 64 | params=params, 65 | json=data, 66 | verify=self.verify_tls, 67 | timeout=self.timeout, 68 | ) 69 | logger.debug("%s %s", method, response.url) 70 | if response.ok: 71 | return response 72 | 73 | logger.debug(response.text) 74 | response.raise_for_status() 75 | raise RuntimeError 76 | -------------------------------------------------------------------------------- /tests/test_subscribers.py: -------------------------------------------------------------------------------- 1 | import types 2 | from unittest import mock 3 | 4 | from base import CachetTestcase 5 | import cachetclient 6 | from fakeapi import FakeHttpClient 7 | 8 | 9 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 10 | class SubscriberTests(CachetTestcase): 11 | 12 | def test_create(self): 13 | client = self.create_client() 14 | sub = client.subscribers.create(email='user@example.com') 15 | self.assertEqual(sub.id, 1) 16 | self.assertEqual(sub.email, 'user@example.com') 17 | 18 | # Count subscribers 19 | self.assertEqual(client.subscribers.count(), 1) 20 | 21 | # Inspect subscribers 22 | sub = next(client.subscribers.list()) 23 | self.assertEqual(sub.id, 1) 24 | self.assertEqual(sub.email, 'user@example.com') 25 | self.assertTrue(sub.is_global) 26 | self.assertIsNotNone(sub.verify_code) 27 | self.assertIsNotNone(sub.verified_at) 28 | self.assertIsNotNone(sub.created_at) 29 | self.assertIsNotNone(sub.updated_at) 30 | 31 | # Delete subscriber 32 | sub.delete() 33 | self.assertEqual(client.subscribers.count(), 0) 34 | 35 | def test_list(self): 36 | """Create a bunch of subscribers and list them""" 37 | client = self.create_client() 38 | num_subs = 20 * 4 + 5 39 | for i in range(num_subs): 40 | client.subscribers.create( 41 | email="user{}@example.com".format(str(i).zfill(3)), 42 | verify=True, 43 | ) 44 | 45 | # Ensure the count matches 46 | self.assertEqual(client.subscribers.count(), num_subs) 47 | 48 | # List should return a generator 49 | self.assertIsInstance(client.subscribers.list(), types.GeneratorType) 50 | 51 | # Request specific page 52 | sub = next(client.subscribers.list(page=2, per_page=10)) 53 | self.assertEqual(sub.id, 11) 54 | 55 | # Delete them all (We cannot delete while iterating) 56 | subs = list(client.subscribers.list()) 57 | self.assertEqual(len(subs), num_subs) 58 | self.assertEqual(len(set(subs)), num_subs) 59 | for sub in subs: 60 | sub.delete() 61 | 62 | # We should have no subs left 63 | self.assertEqual(client.subscribers.count(), 0) 64 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | sys.path.insert(0, os.path.abspath('../')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'cachet-client' 22 | copyright = '2019, Zetta.IO Technology' 23 | author = 'Einar Forselv' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | version = '3.1.1' 27 | release = version 28 | 29 | # The master toctree document. 30 | master_doc = 'index' 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | # Autodoc 39 | 'sphinx.ext.autodoc', 40 | # google style docstrings 41 | 'sphinx.ext.napoleon', 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = 'sphinx_rtd_theme' 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ['_static'] 64 | -------------------------------------------------------------------------------- /cachetclient/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cachetclient import v1 4 | from cachetclient.httpclient import HttpClient 5 | 6 | 7 | def Client( 8 | endpoint: str = None, 9 | api_token: str = None, 10 | version: str = None, 11 | verify_tls: bool = True, 12 | ) -> v1.Client: 13 | """ 14 | Creates a cachet client. Use this fuction to create clients to ensure 15 | compatibility in the future. 16 | 17 | Args: 18 | endpoint (str): The api endpoint. for example 'https://status.examples.test/api/v1'. 19 | The endpoint can also be specified using the ``CACHET_ENDPOINT`` env variable. 20 | api_token (str): The api token. Can also be specified using ``CACHET_API_TOKEN`` env variable. 21 | version (str): The api version. If not specified the version will be derived from the 22 | endpoint url. The value "1" will create a v1 cachet client. 23 | verify_tls (bool): Enable/disable tls verify. When using self signed certificates this has to be ``False``. 24 | """ 25 | if not api_token: 26 | api_token = os.environ.get("CACHET_API_TOKEN") 27 | 28 | if not api_token: 29 | raise ValueError( 30 | "No api_token specified. " 31 | "The endpoint must be supplied in the Client function " 32 | "or through the CACHET_API_TOKEN environment variable." 33 | ) 34 | 35 | if not endpoint: 36 | endpoint = os.environ.get("CACHET_ENDPOINT") 37 | 38 | if not endpoint: 39 | raise ValueError( 40 | "No api endpoint specified. " 41 | "The token must be supplied in the Client function " 42 | "or through the CACHET_ENDPOINT environment variable." 43 | ) 44 | 45 | if not version: 46 | version = detect_version(endpoint) 47 | 48 | return v1.Client(HttpClient(endpoint, api_token, verify_tls=verify_tls)) 49 | 50 | 51 | def detect_version(endpoint: str) -> str: 52 | """ 53 | Detect the api version from endpoint url. 54 | Currently cachet only has a single "v1" endpoint but this may change in the future. 55 | """ 56 | if endpoint.endswith("/v1"): 57 | return "1" 58 | 59 | raise ValueError( 60 | "Cannot determine api version based on endpoint '{}'. " 61 | "If the api version is not present in the url, " 62 | "please supply it on client creation.".format(endpoint) 63 | ) 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 4.0.1 3 | 4 | Drop python 3.5 support. 5 | 6 | # 4.0.0 7 | 8 | ## Tags 9 | 10 | Revamped how we interact with component tags. Previous 11 | versions was limited to inspecting component slugs. 12 | 13 | * ``Component.tags`` now returns a slug:name dict 14 | * ``Component.tag_names`` returns a list of tag names 15 | * ``Component.tag_slugs`` returns a list of slug names 16 | * ``Components.tags`` are now read-only 17 | * ``Components.set_tags()`` will overwrite all tags 18 | * ``Components.add_tag`` and ``add_tags`` can be used to add tags 19 | * All tag lookups are now case insensitive 20 | 21 | ## Other Additions / Fixes 22 | 23 | * Incidents now support the ``occurred_at`` field in cachet 2.4 24 | * Updating components via incidents should no longer cause a 400 error if 25 | `component_id` or `component_status` is missing 26 | 27 | # 3.1.1 28 | 29 | * Added missing resource imports in `v1.__init__` 30 | * Removed irrelevant `__version__` value in `cachetclient.v1.__init__` 31 | 32 | # 3.1.0 33 | 34 | * Added support for schedules (cachet 2.4) 35 | 36 | # 3.0.0 37 | 38 | ## Additions / Improvements 39 | 40 | * Support for metrics and metric points 41 | * Documentation improvements 42 | 43 | ## Breaking changes 44 | 45 | * Fixed class name typo: `IncidentUpdate` properly renamed to `IncidentUpdate` 46 | * `IncidentUpdate.permlink` renamed to `permalink` (in line with the actual field name) 47 | 48 | # 2.0.1 49 | 50 | * Fix Internal Server Error when creating incidents due to 51 | empty `vars`. (Likely a 2.4+ issue) 52 | 53 | # 2.0.0 54 | 55 | * Python 3.8 support 56 | * Fix class name typo `CompontentGroup` -> `ComponentGroup` 57 | * Fix class name typo `CompontentGroupManager` -> `ComponentGroupManager` 58 | * Various other typos 59 | * ComponentGroup now supports the `visible` flag 60 | 61 | # 1.3.0 62 | 63 | * All managers now support `instance_list_from_json` 64 | to greatly ease re-creation of resource objects lists 65 | stored in databases or caches as json. 66 | 67 | # 1.2.0 68 | 69 | * All resource instances can now be re-created using `instance_from_dict` and `instance_from_json`. 70 | * Some doc improvements 71 | 72 | # 1.1.0 73 | 74 | * Downgraded python requirement to 3.5. Currently there are no reasons to require 3.6+ 75 | * `ComponentGroup` now has the `enabled_components` property. 76 | * Tests are now run in py3.5, py3.6 and py3.7 environments using `tox`. 77 | 78 | # 1.0.0 79 | 80 | * First stable version 81 | * Some method signatures have changed 82 | * Richer docstrings + Docs. 83 | 84 | # 0.9.0 85 | 86 | Initial release. We will move towards 1.0 until we are 2.4 complete. 87 | -------------------------------------------------------------------------------- /cachetclient/v1/enums.py: -------------------------------------------------------------------------------- 1 | # Component statuses 2 | #: [1] Operational. The component is working. 3 | COMPONENT_STATUS_OPERATIONAL = 1 4 | #: [2] Performance Issues. The component is experiencing some slowness. 5 | COMPONENT_STATUS_PERFORMANCE_ISSUES = 2 6 | #: [3] Partial Outage. The component may not be working for everybody. This could be a geographical issue for example. 7 | COMPONENT_STATUS_PARTIAL_OUTAGE = 3 8 | #: [4] Major Outage. The component is not working for anybody. 9 | COMPONENT_STATUS_MAJOR_OUTAGE = 4 10 | 11 | #: List of all component statuses 12 | #: 13 | #: Can be used for:: 14 | #: 15 | #: >> status in enums.COMPONENT_STATUS_LIST 16 | #: True 17 | COMPONENT_STATUS_LIST = [ 18 | COMPONENT_STATUS_OPERATIONAL, 19 | COMPONENT_STATUS_PERFORMANCE_ISSUES, 20 | COMPONENT_STATUS_PARTIAL_OUTAGE, 21 | COMPONENT_STATUS_MAJOR_OUTAGE, 22 | ] 23 | 24 | # Component group collapse value 25 | #: [0] No 26 | COMPONENT_GROUP_COLLAPSED_FALSE = 0 27 | #: [1] Yes 28 | COMPONENT_GROUP_COLLAPSED_TRUE = 1 29 | #: [2] Component is not Operational 30 | COMPONENT_GROUP_COLLAPSED_NOT_OPERATIONAL = 2 31 | 32 | # Incident Status 33 | #: [0] Scheduled. This status is reserved for a scheduled status. 34 | INCIDENT_SCHEDULED = 0 35 | #: [1] Investigating. You have reports of a problem and you're currently looking into them. 36 | INCIDENT_INVESTIGATING = 1 37 | #: [2] Identified. You've found the issue and you're working on a fix. 38 | INCIDENT_IDENTIFIED = 2 39 | #: [3] Watching. You've since deployed a fix and you're currently watching the situation. 40 | INCIDENT_WATCHING = 3 41 | #: [4] Fixed. The fix has worked, you're happy to close the incident. 42 | INCIDENT_FIXED = 4 43 | 44 | 45 | def incident_status_human(status: int): 46 | """Get human status from incident status id 47 | 48 | Example:: 49 | 50 | >> incident_status_human(enums.INCIDENT_FIXED) 51 | Fixed 52 | 53 | Args: 54 | status (int): Incident status id 55 | 56 | Returns: 57 | str: Human status 58 | """ 59 | data = { 60 | INCIDENT_SCHEDULED: "Scheduled", 61 | INCIDENT_INVESTIGATING: "Investigating", 62 | INCIDENT_IDENTIFIED: "Identified", 63 | INCIDENT_WATCHING: "Watching", 64 | INCIDENT_FIXED: "Fixed", 65 | } 66 | return data[status] 67 | 68 | 69 | # Schedule Status 70 | #: [0] Upcoming 71 | SCHEDULE_STATUS_UPCOMING = 0 72 | #: [1] In progress 73 | SCHEDULE_STATUS_IN_PROGRESS = 1 74 | #: [2] Completed 75 | SCHEDULE_STATUS_COMPLETE = 2 76 | 77 | SCHEDULE_STATUS_LIST = [ 78 | SCHEDULE_STATUS_UPCOMING, 79 | SCHEDULE_STATUS_IN_PROGRESS, 80 | SCHEDULE_STATUS_COMPLETE, 81 | ] 82 | -------------------------------------------------------------------------------- /tests/test_incidents.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import mock 3 | from requests.exceptions import HTTPError 4 | 5 | from base import CachetTestcase 6 | from fakeapi import FakeHttpClient 7 | from cachetclient.v1 import enums 8 | 9 | 10 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 11 | class IncidentTests(CachetTestcase): 12 | 13 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 14 | def setUp(self): 15 | self.client = self.create_client() 16 | 17 | def test_get(self): 18 | first = self.client.incidents.create(name="Issue 1", message="Descr", status=enums.INCIDENT_INVESTIGATING) 19 | self.client.incidents.create(name="Issue 2", message="Descr", status=enums.INCIDENT_INVESTIGATING) 20 | self.client.incidents.create(name="Issue 3", message="Descr", status=enums.INCIDENT_INVESTIGATING) 21 | 22 | self.assertEqual(self.client.incidents.count(), 3) 23 | 24 | incidents = self.client.incidents.list() 25 | incidents = list(incidents) 26 | self.assertEqual(len(incidents), 3) 27 | 28 | # Re-fetch a single issue 29 | incident = self.client.incidents.get(first.id) 30 | self.assertEqual(first.id, incident.id) 31 | 32 | def test_create(self): 33 | issue = self.client.incidents.create( 34 | name="Something blew up!", 35 | message="We are looking into it", 36 | status=enums.INCIDENT_INVESTIGATING, 37 | ) 38 | 39 | self.assertEqual(issue.id, 1) 40 | self.assertEqual(issue.name, "Something blew up!") 41 | self.assertEqual(issue.message, "We are looking into it") 42 | self.assertEqual(issue.status, enums.INCIDENT_INVESTIGATING) 43 | self.assertEqual(issue.component_id, None) 44 | self.assertEqual(issue.visible, True) 45 | self.assertEqual(issue.notify, True) 46 | self.assertEqual(issue.human_status, 'Investigating') 47 | self.assertIsInstance(issue.created_at, datetime) 48 | self.assertIsInstance(issue.updated_at, datetime) 49 | self.assertIsInstance(issue.scheduled_at, datetime) 50 | 51 | # Do an update on the resource 52 | issue.name = "Something probably blew up?!" 53 | issue = issue.update() 54 | self.assertEqual(issue.name, "Something probably blew up?!") 55 | 56 | # Update directly 57 | issue = self.client.incidents.update( 58 | issue.id, 59 | name="Something probably blew up?!", 60 | message="All good", 61 | status=enums.INCIDENT_FIXED, 62 | visible=True, 63 | ) 64 | self.assertEqual(issue.id, 1) 65 | self.assertEqual(issue.status, enums.INCIDENT_FIXED) 66 | self.assertIsInstance(issue.created_at, datetime) 67 | self.assertIsInstance(issue.updated_at, datetime) 68 | self.assertIsInstance(issue.scheduled_at, datetime) 69 | 70 | issue.delete() 71 | -------------------------------------------------------------------------------- /tests/test_incident_updates.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from datetime import datetime 3 | 4 | from base import CachetTestcase 5 | from fakeapi import FakeHttpClient 6 | from cachetclient.v1 import enums 7 | 8 | 9 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 10 | class IncidentUpdatesTests(CachetTestcase): 11 | 12 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 13 | def setUp(self): 14 | self.client = self.create_client() 15 | 16 | def test_create(self): 17 | incident = self.client.incidents.create( 18 | name="Boom!", 19 | message="We are investigating", 20 | status=enums.INCIDENT_INVESTIGATING, 21 | ) 22 | 23 | # Add 3 updates 24 | first = self.client.incident_updates.create( 25 | incident_id=incident.id, 26 | status=enums.INCIDENT_IDENTIFIED, 27 | message="We have located the issue", 28 | ) 29 | # Test all properties 30 | self.assertEqual(first.id, 1) 31 | self.assertEqual(first.incident_id, 1) 32 | self.assertEqual(first.status, enums.INCIDENT_IDENTIFIED) 33 | self.assertEqual(first.message, "We have located the issue") 34 | self.assertEqual(first.user_id, 1) 35 | self.assertIsInstance(first.created_at, datetime) 36 | self.assertIsInstance(first.updated_at, datetime) 37 | self.assertEqual(first.human_status, "Identified") 38 | self.assertIsInstance(first.permalink, str) 39 | 40 | self.client.incident_updates.create( 41 | incident_id=incident.id, 42 | status=enums.INCIDENT_WATCHING, 43 | message="We have located the issue", 44 | ) 45 | self.client.incident_updates.create( 46 | incident_id=incident.id, 47 | status=enums.INCIDENT_FIXED, 48 | message="We have located the issue", 49 | ) 50 | 51 | self.assertEqual(self.client.incident_updates.count(incident.id), 3) 52 | 53 | # List and compare 54 | updates = list(incident.updates()) 55 | self.assertEqual(len(updates), 3) 56 | self.assertEqual( 57 | [{k: i.attrs[k] for k in ['id', 'incident_id', 'status', 'message']} for i in updates], 58 | [{'id': 1, 'incident_id': 1, 'status': 2, 'message': 'We have located the issue'}, 59 | {'id': 2, 'incident_id': 1, 'status': 3, 'message': 'We have located the issue'}, 60 | {'id': 3, 'incident_id': 1, 'status': 4, 'message': 'We have located the issue'}] 61 | ) 62 | 63 | # Update an entry 64 | entry = updates[-1] 65 | entry.status = enums.INCIDENT_INVESTIGATING 66 | entry.message = "Lookin into it.." 67 | entry = entry.update() 68 | 69 | # Manually re-fetch 70 | updated_entry = self.client.incident_updates.get(entry.incident_id, entry.id) 71 | self.assertEqual(updated_entry.status, enums.INCIDENT_INVESTIGATING) 72 | self.assertEqual(updated_entry.message, "Lookin into it..") 73 | -------------------------------------------------------------------------------- /cachetclient/v1/metric_points.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator, Optional 3 | 4 | from cachetclient.base import Manager, Resource 5 | from cachetclient import utils 6 | 7 | 8 | class MetricPoint(Resource): 9 | @property 10 | def id(self) -> int: 11 | """int: unique id of the metric point""" 12 | return self.get("id") 13 | 14 | @property 15 | def metric_id(self) -> int: 16 | """int: Get or set metic id for this metric point""" 17 | return self.get("metric_id") 18 | 19 | @metric_id.setter 20 | def metric_id(self, value: int): 21 | self._data["metric_id"] = value 22 | 23 | @property 24 | def value(self) -> float: 25 | """float: Value to plot on the metric graph""" 26 | return self.get("value") 27 | 28 | @value.setter 29 | def value(self, value: float): 30 | self._data["value"] = value 31 | 32 | @property 33 | def created_at(self) -> Optional[datetime]: 34 | """datetime: When the metric point was created""" 35 | return utils.to_datetime(self.get("created_at")) 36 | 37 | @property 38 | def updated_at(self) -> Optional[datetime]: 39 | """datetime: Last time the issue was updated""" 40 | return utils.to_datetime(self.get("updated_at")) 41 | 42 | @property 43 | def counter(self) -> int: 44 | """int: Show the actual calculated value""" 45 | return self.get("counter") 46 | 47 | @counter.setter 48 | def counter(self, value: float): 49 | self._data["counter"] = value 50 | 51 | @property 52 | def calculated_value(self) -> float: 53 | """float: The calculated value on metric graph""" 54 | return self.get("calculated_value") 55 | 56 | @calculated_value.setter 57 | def calculated_value(self, value: float): 58 | self._data["calculated_value"] = value 59 | 60 | 61 | class MetricPointsManager(Manager): 62 | resource_class = MetricPoint 63 | path = "metrics/{}/points" 64 | 65 | def create(self, *, metric_id: int, value: float) -> MetricPoint: 66 | """ 67 | Create an metric point 68 | 69 | Keyword Args: 70 | metric_id (int): The metric to tag with the point 71 | value (fload): Metric point value for graph 72 | 73 | Returns: 74 | :py:data:`MetricPoint` instance 75 | """ 76 | return self._create(self.path.format(metric_id), {"value": value}) 77 | 78 | def count(self, metric_id) -> int: 79 | """ 80 | Count the number of metric points for a metric 81 | 82 | Args: 83 | metric_id (int): The metric 84 | 85 | Returns: 86 | int: Number of metric points for the metric 87 | """ 88 | return self._count(self.path.format(metric_id)) 89 | 90 | def list( 91 | self, metric_id: int, page: int = 1, per_page: int = 20 92 | ) -> Generator[MetricPoint, None, None]: 93 | """ 94 | List updates for a metric 95 | 96 | Args: 97 | metric_id: The metric id to list updates 98 | 99 | Keyword Args: 100 | page (int): The first page to request 101 | per_page (int): Entries per page 102 | 103 | Return: 104 | Generator of :py:data:`MetricPoint` 105 | """ 106 | yield from self._list_paginated( 107 | self.path.format(metric_id), page=page, per_page=per_page 108 | ) 109 | 110 | def delete(self, metric_id: int, point_id: int) -> None: 111 | """ 112 | Delete a metric point 113 | """ 114 | self._delete(self.path.format(metric_id), point_id) 115 | -------------------------------------------------------------------------------- /cachetclient/v1/subscribers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator, List, Optional 3 | 4 | from cachetclient.base import Manager, Resource 5 | from cachetclient import utils 6 | 7 | 8 | class Subscriber(Resource): 9 | @property 10 | def id(self) -> int: 11 | """int: Resource ID""" 12 | return int(self._data["id"]) 13 | 14 | @property 15 | def email(self) -> str: 16 | """str: email address""" 17 | return self._data["email"] 18 | 19 | @property 20 | def verify_code(self) -> str: 21 | """str: Auto generated unique verify code""" 22 | return self._data["verify_code"] 23 | 24 | @property 25 | def is_global(self) -> bool: 26 | """bool: Is the user subscribed to all components?""" 27 | return self._data["global"] 28 | 29 | @property 30 | def created_at(self) -> Optional[datetime]: 31 | """datetime: When the subscription was created""" 32 | return utils.to_datetime(self.get("created_at")) 33 | 34 | @property 35 | def updated_at(self) -> Optional[datetime]: 36 | """datetime: Last time the subscription was updated""" 37 | return utils.to_datetime(self.get("updated_at")) 38 | 39 | @property 40 | def verified_at(self) -> Optional[datetime]: 41 | """datetime: When the subscription was verified. ``None`` if not verified""" 42 | return utils.to_datetime(self.get("verified_at")) 43 | 44 | def __str__(self) -> str: 45 | return "".format(self.id, self.email) 46 | 47 | 48 | class SubscriberManager(Manager): 49 | """Manager for subscriber endpoints""" 50 | 51 | resource_class = Subscriber 52 | path = "subscribers" 53 | 54 | def create( 55 | self, *, email: str, components: List[int] = None, verify: bool = True 56 | ) -> Subscriber: 57 | """Create a subscriber. 58 | If a subscriber already exists the existing one will be returned. 59 | Note that this endoint cannot be used to edit the user. 60 | 61 | Keyword Args: 62 | email (str): Email address to subscribe 63 | components (List[int]): The components to subscribe to. If omitted all components are subscribed. 64 | verify (bool): Verification status. If ``False`` a verification email is sent to the user 65 | 66 | Returns: 67 | :py:data:`Subscriber` instance 68 | """ 69 | return self._create( 70 | self.path, 71 | { 72 | "email": email, 73 | "components": components, 74 | "verify": verify, 75 | }, 76 | ) 77 | 78 | def list( 79 | self, page: int = 1, per_page: int = 20 80 | ) -> Generator[Subscriber, None, None]: 81 | """List all subscribers 82 | 83 | Keyword Args: 84 | page (int): The page to start listing 85 | per_page: Number of entries per page 86 | 87 | Returns: 88 | Generator of Subscriber instances 89 | """ 90 | yield from self._list_paginated(self.path, page=page, per_page=per_page) 91 | 92 | def delete(self, subscriber_id: int) -> None: 93 | """Delete a specific subscriber id 94 | 95 | Args: 96 | subscriber_id (int): Subscriber id to delete 97 | 98 | Raises: 99 | :py:data:`requests.exceptions.HttpError`: if subscriber do not exist 100 | """ 101 | self._delete(self.path, subscriber_id) 102 | 103 | def count(self) -> int: 104 | """Count the total number of subscribers 105 | 106 | Returns: 107 | int: Number of subscribers 108 | """ 109 | return self._count(self.path) 110 | -------------------------------------------------------------------------------- /tests/test_components.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import mock 3 | from requests.exceptions import HTTPError 4 | 5 | from base import CachetTestcase 6 | from fakeapi import FakeHttpClient 7 | from cachetclient.v1 import enums 8 | 9 | 10 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 11 | class ComponentsTests(CachetTestcase): 12 | 13 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 14 | def setUp(self): 15 | self.client = self.create_client() 16 | 17 | def create_component( 18 | self, 19 | client, 20 | status=enums.COMPONENT_STATUS_OPERATIONAL, 21 | name="API Server", 22 | description="General API server"): 23 | 24 | return client.components.create( 25 | name=name, 26 | status=status, 27 | description=description, 28 | ) 29 | 30 | def test_count(self): 31 | """Count components""" 32 | client = self.create_client() 33 | self.assertEqual(client.components.count(), 0) 34 | 35 | def test_create(self): 36 | """Create and obtain component""" 37 | self.create_component(self.client) 38 | self.assertEqual(self.client.components.count(), 1) 39 | 40 | comp = next(self.client.components.list()) 41 | self.assertEqual(comp.id, 1) 42 | self.assertEqual(comp.name, "API Server") 43 | self.assertEqual(comp.description, "General API server") 44 | self.assertEqual(comp.group_id, None) 45 | self.assertEqual(comp.link, None) 46 | self.assertEqual(comp.status, enums.COMPONENT_STATUS_OPERATIONAL) 47 | self.assertEqual(comp.status_name, "Operational") 48 | self.assertIsInstance(comp.created_at, datetime) 49 | self.assertIsInstance(comp.updated_at, datetime) 50 | 51 | comp = self.client.components.get(1) 52 | self.assertEqual(comp.id, 1) 53 | 54 | def test_delete(self): 55 | """Create and delete component""" 56 | self.create_component(self.client) 57 | self.assertEqual(self.client.components.count(), 1) 58 | comp = next(self.client.components.list()) 59 | comp.delete() 60 | self.assertEqual(self.client.components.count(), 0) 61 | 62 | def test_delete_nonexist(self): 63 | """Delete non-existant component""" 64 | with self.assertRaises(HTTPError): 65 | self.client.components.delete(1337) 66 | 67 | def test_tags(self): 68 | """Test tags""" 69 | comp = self.create_component(self.client) 70 | comp.add_tag('Test Tag') 71 | self.assertTrue(comp.has_tag(name='Test Tag')) 72 | self.assertTrue(comp.has_tag(name='test tag')) 73 | self.assertFalse(comp.has_tag(name='thing')) 74 | comp.del_tag(name="Test Tag") 75 | self.assertFalse(comp.has_tag("Test Tag")) 76 | 77 | comp.add_tag('Tag 1') 78 | comp.add_tag('Tag 2') 79 | comp = comp = comp.update() 80 | self.assertTrue(comp.has_tag('Tag 1')) 81 | self.assertTrue(comp.has_tag('Tag 2')) 82 | self.assertTrue(comp.has_tag(slug='tag-1')) 83 | self.assertTrue(comp.has_tag(slug='tag-2')) 84 | self.assertFalse(comp.has_tag('test')) 85 | self.assertEqual(sorted(comp.tag_names), ["Tag 1", "Tag 2"]) 86 | 87 | comp.add_tag("Tag 3") 88 | self.assertEqual(len(comp.tags), 3) 89 | comp.del_tag(slug="tag-1") 90 | self.assertEqual(len(comp.tags), 2) 91 | comp = comp.update() 92 | self.assertFalse(comp.has_tag(slug="tag-1")) 93 | self.assertFalse(comp.has_tag(name="tag 1")) 94 | self.assertTrue(comp.has_tag(slug="tag-2")) 95 | self.assertTrue(comp.has_tag(name="tag 2")) 96 | self.assertTrue(comp.has_tag(slug="tag-3")) 97 | self.assertTrue(comp.has_tag(name="tag 3")) 98 | self.assertEqual(len(comp.tags), 2) 99 | -------------------------------------------------------------------------------- /cachetclient/v1/schedules.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from cachetclient.v1 import enums 3 | from typing import Generator, Optional 4 | 5 | from cachetclient.base import Resource, Manager 6 | from cachetclient import utils 7 | 8 | 9 | class Schedule(Resource): 10 | @property 11 | def id(self) -> int: 12 | """int: Resource ID""" 13 | return self.get("id") 14 | 15 | @property 16 | def name(self) -> str: 17 | """str: Name of the scheduled event""" 18 | return self.get("name") 19 | 20 | @property 21 | def message(self) -> str: 22 | """str: Message string""" 23 | return self.get("message") 24 | 25 | @property 26 | def status(self) -> int: 27 | """int: Status of the scheduled event""" 28 | return self.get("status") 29 | 30 | @property 31 | def scheduled_at(self) -> Optional[datetime]: 32 | """datetime: When the event is schedule for""" 33 | return utils.to_datetime(self.get("scheduled_at")) 34 | 35 | @property 36 | def completed_at(self) -> Optional[datetime]: 37 | """datetime: When the event is completed""" 38 | return utils.to_datetime(self.get("completed_at")) 39 | 40 | 41 | class ScheduleManager(Manager): 42 | path = "schedules" 43 | resource_class = Schedule 44 | 45 | def create( 46 | self, 47 | *, 48 | name: str, 49 | status: int, 50 | message: str = None, 51 | scheduled_at: datetime = None, 52 | completed_at: datetime = None, 53 | notify: bool = True 54 | ): 55 | """Create a schedule. 56 | 57 | Keyword Args: 58 | name (str): Name of the scheduled event 59 | status (int): Schedule status. See ``enums`` 60 | mesage (str): Message string 61 | scheduled_at (datetime): When the event starts 62 | completed_at (datetime): When the event ends 63 | notify (bool): Notify subscribers 64 | 65 | Returns: 66 | :py:class:`Schedule` instance 67 | """ 68 | if status not in enums.SCHEDULE_STATUS_LIST: 69 | raise ValueError( 70 | "Invalid status id '{}'. Valid values :{}".format( 71 | status, 72 | enums.SCHEDULE_STATUS_LIST, 73 | ) 74 | ) 75 | 76 | return self._create( 77 | self.path, 78 | self._build_data_dict( 79 | name=name, 80 | message=message, 81 | status=enums.SCHEDULE_STATUS_UPCOMING, 82 | scheduled_at=scheduled_at.strftime("%Y-%m-%d %H:%M") if scheduled_at else None, 83 | completed_at=completed_at.strftime("%Y-%m-%d %H:%M") if completed_at else None, 84 | notify=0, 85 | ), 86 | ) 87 | 88 | def update( 89 | self, 90 | schedule_id: int, 91 | *, 92 | name: str, 93 | status: int, 94 | message: str = None, 95 | scheduled_at: datetime = None, 96 | **kwargs 97 | ) -> Schedule: 98 | """Update a Schedule by id. 99 | 100 | Args: 101 | schedule_id (int): The schedule to update 102 | 103 | Keyword Args: 104 | status (int): Status of the schedule (see enums) 105 | name (str): New name 106 | description (str): New description 107 | 108 | Returns: 109 | Updated Schedule from server 110 | """ 111 | return self._update( 112 | self.path, 113 | schedule_id, 114 | self._build_data_dict( 115 | name=name, status=status, message=message, scheduled_at=scheduled_at 116 | ), 117 | ) 118 | 119 | def list( 120 | self, page: int = 1, per_page: int = 20 121 | ) -> Generator[Schedule, None, None]: 122 | """List all schedules 123 | 124 | Keyword Args: 125 | page (int): The page to start listing 126 | per_page (int): Number of entries per page 127 | 128 | Returns: 129 | Generator of Schedules instances 130 | """ 131 | yield from self._list_paginated(self.path, page=page, per_page=per_page) 132 | 133 | def get(self, schedule_id: int) -> Schedule: 134 | """Get a schedule by id 135 | 136 | Args: 137 | schedule_id (int): Id of the schedule 138 | 139 | Returns: 140 | Schedule instance 141 | 142 | Raises: 143 | HttpError: if not found 144 | """ 145 | return self._get(self.path, schedule_id) 146 | 147 | def delete(self, schedule_id: int) -> None: 148 | """Delete a schedule 149 | 150 | Args: 151 | schedule_id (int): Id of the schedule 152 | 153 | Raises: 154 | HTTPError: if schedule do not exist 155 | """ 156 | self._delete(self.path, schedule_id) 157 | 158 | def count(self) -> int: 159 | """Count the total number of scheduled events 160 | 161 | Returns: 162 | int: Number of subscribers 163 | """ 164 | return self._count(self.path) 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi](https://badge.fury.io/py/cachet-client.svg)](https://pypi.python.org/pypi/cachet-client) 2 | [![travis](https://api.travis-ci.org/ZettaIO/cachet-client.svg?branch=master)](https://travis-ci.org/ZettaIO/cachet-client) [![Documentation Status](https://readthedocs.org/projects/cachet-client/badge/?version=latest&nop)](https://cachet-client.readthedocs.io/en/latest/?badge=latest) 3 | 4 | # cachet-client 5 | 6 | A python 3.6+ API client for the open source status page system 7 | [Cachet](https://github.com/CachetHQ/Cachet). 8 | 9 | * [cachet-client on github](https://github.com/ZettaIO/cachet-client) 10 | * [cachet-client on PyPI](https://pypi.org/project/cachet-client/) 11 | * [cachet-client documentation](https://cachet-client.readthedocs.io/) 12 | 13 | The goal of this package is to create a user friendly interface 14 | to the Cachet API. 15 | 16 | * Resources are returned as objects clearly separating read only 17 | properties from the ones we can change. The raw json response 18 | is always available in an `attrs` property 19 | * Active use of type hints throughout the entire codebase 20 | making code completion a breeze 21 | * Proper pagination under the hood. Method listing resources 22 | will return generators. You can configure the start page and 23 | page size that fits the situation. Each new page leads to 24 | a new http request. 25 | * Client is using a single session regardless of resource type 26 | making more complex work a lot faster (connection reuse) 27 | * A very extensive set of tests/unit tests. 28 | * Easy to extend and test 29 | * Documentation 30 | 31 | **Please don't hesitate opening an issue about anything related to this package.** 32 | 33 | ## Install 34 | 35 | ``` 36 | pip install cachet-client 37 | ``` 38 | 39 | # Example 40 | 41 | ```python 42 | import cachetclient 43 | from cachetclient.v1 import enums 44 | 45 | client = cachetclient.Client( 46 | endpoint='https://status.test/api/v1', 47 | api_token='secrettoken', 48 | ) 49 | ``` 50 | 51 | Check if api is responding 52 | 53 | ```python 54 | if client.ping(): 55 | print("Cachet is up and running!") 56 | ``` 57 | 58 | Create and delete a subscriber 59 | 60 | ```python 61 | sub = client.subscribers.create(email='user@example.test', verify=True) 62 | sub.delete() 63 | ``` 64 | 65 | List all subscribers paginated (generator). Each new page is fetched 66 | from the server under the hood. 67 | 68 | ```python 69 | for sub in client.subscribers.list(page=1, per_page=100): 70 | print(sub.id, sub.email) 71 | ``` 72 | 73 | Create a component issue 74 | 75 | ```python 76 | issue = client.incidents.create( 77 | name="Something blew up!", 78 | message="We are looking into it", 79 | status=enums.INCIDENT_INVESTIGATING, 80 | # Optional for component issues 81 | component_id=mycomponent.id, 82 | component_status=enums.COMPONENT_STATUS_MAJOR_OUTAGE, 83 | ) 84 | ``` 85 | 86 | .. and most other features supported by the Cachet API 87 | 88 | 89 | ## Local Development 90 | 91 | Local setup: 92 | 93 | ```bash 94 | python -m virtualenv .venv 95 | . .venv/bin/activate 96 | pip install -e . 97 | ``` 98 | 99 | ## Tests 100 | 101 | This project has a fairly extensive test setup. 102 | 103 | * Unit tests are located in `tests/` including a fake 104 | implementation of the Cachet API. 105 | * A simpler test script under `extras/live_run.py` that 106 | needs a running test instance of Cachet. 107 | 108 | ### Running unit tests 109 | 110 | ```bash 111 | pip install -r tests/requirements.txt 112 | tox 113 | 114 | # Optionally 115 | tox -e pep8 # for pep8 run only 116 | tox -e py36 # tests only 117 | 118 | 119 | # Running tests with pytest also works, but this works poorly in combination with environment variables for the live test script (tox separates environments) 120 | pytest tests/ 121 | ``` 122 | 123 | ### Testing with real Cachet service 124 | 125 | Do not run this script against a system in production. 126 | This is only for a test service. 127 | Cachet can easily be set up locally with docker: https://github.com/CachetHQ/Docker 128 | 129 | Optionally we can run cachet from source: https://github.com/CachetHQ/Docker 130 | 131 | A local setup is also located in the root or the repo (`docker-compose.yaml`). 132 | 133 | You need to set the following environment variables. 134 | 135 | ```bash 136 | CACHET_ENDPOINT 137 | CACHET_API_TOKEN 138 | ``` 139 | 140 | Running tests: 141 | 142 | ```bash 143 | python extras/live_run.py 144 | ... 145 | ================================================= 146 | Number of tests : 10 147 | Successful : 10 148 | Failure : 0 149 | Percentage passed : 100.0% 150 | ================================================= 151 | ``` 152 | 153 | ## Building Docs 154 | 155 | ```bash 156 | pip install -r docs/requirements.txt 157 | python setup.py build_sphinx 158 | ``` 159 | 160 | ## Contributing 161 | 162 | Do not hesitate opening issues or submit completed 163 | or partial pull requests. Contributors of all 164 | experience levels are welcome. 165 | 166 | --- 167 | This project is sponsored by [zetta.io](https://www.zetta.io) 168 | 169 | [![zetta.io](https://raw.githubusercontent.com/ZettaIO/cachet-client/master/.github/logo.png)](https://www.zetta.io) 170 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Documentation testing 3 | 4 | Inspired by: https://github.com/cprogrammer1994/ModernGL/blob/master/tests/test_documentation.py 5 | by Szabolcs Dombi 6 | 7 | This version is simplified: 8 | 9 | * Only test if the attribute or method/function is present in the class. Parameters are not inspected. 10 | * Include ignore pattern in the implemented set 11 | """ 12 | import os 13 | import re 14 | import types 15 | import unittest 16 | from importlib import import_module 17 | 18 | 19 | class DocTestCase(unittest.TestCase): 20 | """ 21 | Test reference docs 22 | """ 23 | def validate(self, filename, module, classname=None, ignore=None): 24 | """ 25 | Finds all automethod and autoattribute statements in an rst file 26 | comparing them to the attributes found in the actual class 27 | """ 28 | if ignore is None: 29 | ignore = [] 30 | 31 | with open(os.path.normpath(os.path.join('docs', 'reference', filename))) as f: 32 | docs = f.read() 33 | 34 | module = import_module(module) 35 | 36 | # Inspect class 37 | if classname: 38 | methods = re.findall(r'^\.\. automethod:: ([^\(\n]+)', docs, flags=re.M) 39 | attributes = re.findall(r'^\.\. autoattribute:: ([^\n]+)', docs, flags=re.M) 40 | 41 | documented = set(filter(lambda x: x.startswith(classname + '.'), [a for a in methods] + attributes)) 42 | implemented = set(classname + '.' + x for x in dir(getattr(module, classname)) 43 | if not x.startswith('_') or x in ['__init__', '__call__']) 44 | ignored = set(classname + '.' + x for x in ignore) 45 | # Inspect module 46 | else: 47 | # Only inspect functions for now 48 | functions = re.findall(r'^\.\. autofunction:: ([^\(\n]+)', docs, flags=re.M) 49 | documented = set(functions) 50 | ignored = set(ignore) 51 | implemented = set(func for func in dir(module) if isinstance(getattr(module, func), types.FunctionType)) 52 | 53 | self.assertSetEqual(implemented - documented - ignored, set(), msg='Implemented but not Documented') 54 | self.assertSetEqual(documented - implemented - ignored, set(), msg='Documented but not Implemented') 55 | 56 | def test_client(self): 57 | self.validate('cachetclient.client.rst', 'cachetclient.client', ignore=['detect_version']) 58 | 59 | def test_component_group(self): 60 | self.validate('cachetclient.v1.component_groups.rst', 'cachetclient.v1.component_groups', classname='ComponentGroup') 61 | 62 | def test_component_group_manager(self): 63 | self.validate('cachetclient.v1.component_groups.rst', 'cachetclient.v1.component_groups', classname='ComponentGroupManager') 64 | 65 | def test_component(self): 66 | self.validate('cachetclient.v1.components.rst', 'cachetclient.v1.components', classname='Component') 67 | 68 | def test_component_manager(self): 69 | self.validate('cachetclient.v1.components.rst', 'cachetclient.v1.components', classname='ComponentManager') 70 | 71 | def test_enums(self): 72 | self.validate('cachetclient.v1.enums.rst', 'cachetclient.v1.enums') 73 | 74 | def test_incident_update(self): 75 | self.validate('cachetclient.v1.incident_updates.rst', 'cachetclient.v1.incident_updates', classname='IncidentUpdate') 76 | 77 | def test_incident_update_manager(self): 78 | self.validate('cachetclient.v1.incident_updates.rst', 'cachetclient.v1.incident_updates', classname='IncidentUpdatesManager') 79 | 80 | def test_incident(self): 81 | self.validate('cachetclient.v1.incidents.rst', 'cachetclient.v1.incidents', classname='Incident') 82 | 83 | def test_incident_manager(self): 84 | self.validate('cachetclient.v1.incidents.rst', 'cachetclient.v1.incidents', classname='IncidentManager') 85 | 86 | def test_metric_points(self): 87 | self.validate('cachetclient.v1.metric_points.rst', 'cachetclient.v1.metric_points', classname='MetricPoint') 88 | 89 | def test_metric_points_manager(self): 90 | self.validate('cachetclient.v1.metric_points.rst', 'cachetclient.v1.metric_points', classname='MetricPointsManager') 91 | 92 | def test_metrics(self): 93 | self.validate('cachetclient.v1.metrics.rst', 'cachetclient.v1.metrics', classname='Metric') 94 | 95 | def test_metrics_manager(self): 96 | self.validate('cachetclient.v1.metrics.rst', 'cachetclient.v1.metrics', classname='MetricsManager') 97 | 98 | def test_ping(self): 99 | self.validate('cachetclient.v1.ping.rst', 'cachetclient.v1.ping', classname='PingManager', ignore=['instance_from_dict', 'instance_list_from_json', 'instance_from_json']) 100 | 101 | def test_subscribers(self): 102 | self.validate('cachetclient.v1.subscribers.rst', 'cachetclient.v1.subscribers', classname='Subscriber') 103 | 104 | def test_subscribers_manager(self): 105 | self.validate('cachetclient.v1.subscribers.rst', 'cachetclient.v1.subscribers', classname='SubscriberManager') 106 | 107 | def test_version(self): 108 | self.validate('cachetclient.v1.version.rst', 'cachetclient.v1.version', classname='Version', ignore=['delete', 'update']) 109 | 110 | def test_version(self): 111 | self.validate('cachetclient.v1.version.rst', 'cachetclient.v1.version', classname='VersionManager') 112 | -------------------------------------------------------------------------------- /tests/test_component_groups.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from unittest import mock 4 | from requests.exceptions import HTTPError 5 | 6 | from base import CachetTestcase 7 | from fakeapi import FakeHttpClient 8 | from cachetclient.v1 import enums 9 | 10 | 11 | @mock.patch('cachetclient.client.HttpClient', new=FakeHttpClient) 12 | class ComponentGroupTests(CachetTestcase): 13 | 14 | def test_count(self): 15 | """Count groups""" 16 | client = self.create_client() 17 | self.assertEqual(client.component_groups.count(), 0) 18 | 19 | def test_create(self): 20 | """Create and obtain groups""" 21 | client = self.create_client() 22 | client.component_groups.create(name="Global Services", visible=True) 23 | self.assertEqual(client.component_groups.count(), 1) 24 | 25 | # Check attributes 26 | group = next(client.component_groups.list()) 27 | self.assertEqual(group.id, 1) 28 | self.assertEqual(group.visible, True) 29 | self.assertEqual(group.name, "Global Services") 30 | self.assertEqual(group.collapsed, enums.COMPONENT_GROUP_COLLAPSED_FALSE) 31 | self.assertEqual(group.order, 0) 32 | self.assertIsInstance(group.created_at, datetime) 33 | self.assertIsNotNone(group.updated_at, datetime) 34 | self.assertFalse(group.is_collapsed, False) 35 | self.assertTrue(group.is_open) 36 | self.assertTrue(group.is_operational) 37 | self.assertIsInstance(group.enabled_components, list) 38 | 39 | # update group 40 | group.name = "Global Websites" 41 | group.order = 2 42 | group.visible = True 43 | group.collapsed = enums.COMPONENT_GROUP_COLLAPSED_TRUE 44 | group = group.update() 45 | self.assertEqual(group.id, 1) 46 | self.assertEqual(group.visible, True) 47 | self.assertEqual(group.name, "Global Websites") 48 | self.assertEqual(group.collapsed, enums.COMPONENT_GROUP_COLLAPSED_TRUE) 49 | self.assertEqual(group.order, 2) 50 | self.assertIsInstance(group.created_at, datetime) 51 | self.assertIsNotNone(group.updated_at, datetime) 52 | self.assertTrue(group.is_collapsed) 53 | self.assertFalse(group.is_open) 54 | self.assertTrue(group.is_operational) 55 | 56 | # Re-fetch by id and delete 57 | group = client.component_groups.get(1) 58 | self.assertEqual(group.id, 1) 59 | group.delete() 60 | 61 | def test_get_nonexist(self): 62 | client = self.create_client() 63 | with self.assertRaises(HTTPError): 64 | client.component_groups.get(1337) 65 | 66 | def test_delete(self): 67 | """Create and delete component""" 68 | client = self.create_client() 69 | client.component_groups.create(name="Global Services") 70 | self.assertEqual(client.component_groups.count(), 1) 71 | group = next(client.component_groups.list()) 72 | group.delete() 73 | self.assertEqual(client.component_groups.count(), 0) 74 | 75 | def test_delete_nonexist(self): 76 | """Delete non-exsitent component""" 77 | client = self.create_client() 78 | with self.assertRaises(HTTPError): 79 | client.component_groups.delete(1337) 80 | 81 | def test_self_update(self): 82 | """Test self updating resource""" 83 | client = self.create_client() 84 | client.component_groups.create(name="Global Services") 85 | group = next(client.component_groups.list()) 86 | group.name = "Global Stuff" 87 | group.order = 1 88 | group.visible = True 89 | group.collapsed = enums.COMPONENT_GROUP_COLLAPSED_TRUE 90 | 91 | new_group = group.update() 92 | self.assertEqual(new_group.id, 1) 93 | self.assertEqual(new_group.name, "Global Stuff") 94 | self.assertEqual(new_group.order, 1) 95 | self.assertEqual(new_group.visible, True) 96 | self.assertEqual(new_group.collapsed, enums.COMPONENT_GROUP_COLLAPSED_TRUE) 97 | 98 | def test_instance_from(self): 99 | """Recreate instance from json or dict""" 100 | client = self.create_client() 101 | client.component_groups.create(name="Global Services") 102 | group = next(client.component_groups.list()) 103 | 104 | new_group = client.component_groups.instance_from_dict(group.attrs) 105 | self.assertEqual(new_group.id, 1) 106 | self.assertEqual(new_group.name, "Global Services") 107 | self.assertIsInstance(new_group.enabled_components, list) 108 | 109 | new_group = client.component_groups.instance_from_json(json.dumps(group.attrs)) 110 | self.assertEqual(new_group.id, 1) 111 | self.assertEqual(new_group.name, "Global Services") 112 | self.assertIsInstance(new_group.enabled_components, list) 113 | 114 | # from json list 115 | json_data = json.dumps([group.attrs, group.attrs, group.attrs]) 116 | groups = client.component_groups.instance_list_from_json(json_data) 117 | self.assertEqual(len(groups), 3) 118 | group = groups[1] 119 | self.assertEqual(new_group.id, 1) 120 | self.assertEqual(new_group.name, "Global Services") 121 | self.assertIsInstance(new_group.enabled_components, list) 122 | 123 | # Attempt to deserialize a single object as a list 124 | with self.assertRaises(ValueError): 125 | groups = client.component_groups.instance_list_from_json(json.dumps(group.attrs)) 126 | -------------------------------------------------------------------------------- /cachetclient/v1/incident_updates.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator, Optional 3 | 4 | from cachetclient.base import Manager, Resource 5 | from cachetclient import utils 6 | 7 | 8 | class IncidentUpdate(Resource): 9 | @property 10 | def id(self) -> int: 11 | """int: Resource id""" 12 | return self.get("id") 13 | 14 | @property 15 | def incident_id(self) -> int: 16 | """int: The incident id this update belongs to""" 17 | return self.get("incident_id") 18 | 19 | @property 20 | def status(self) -> int: 21 | """int: Get or set incident status. See :py:data:`enums`.""" 22 | return self.get("status") 23 | 24 | @status.setter 25 | def status(self, value: int): 26 | self._data["status"] = value 27 | 28 | @property 29 | def message(self) -> str: 30 | """str: Get or set message""" 31 | return self.get("message") 32 | 33 | @message.setter 34 | def message(self, value: str): 35 | self._data["message"] = value 36 | 37 | @property 38 | def user_id(self) -> int: 39 | """int: The user id creating the update""" 40 | return self.get("user_id") 41 | 42 | @property 43 | def created_at(self) -> Optional[datetime]: 44 | """datetime: when the resource was created""" 45 | return utils.to_datetime(self.get("created_at")) 46 | 47 | @property 48 | def updated_at(self) -> Optional[datetime]: 49 | """datetime: When the resource as last updated""" 50 | return utils.to_datetime(self.get("updated_at")) 51 | 52 | @property 53 | def human_status(self) -> str: 54 | """str: Human readable status""" 55 | return self.get("human_status") 56 | 57 | @property 58 | def permalink(self) -> str: 59 | """str: Permanent url to the incident update""" 60 | return self.get("permalink") 61 | 62 | def update(self) -> "IncidentUpdate": 63 | """ 64 | Update/save changes 65 | 66 | Returns: 67 | Updated IncidentUpdate instance 68 | """ 69 | return self._manager.update(**self.attrs) 70 | 71 | def delete(self) -> None: 72 | """Deletes the incident update""" 73 | self._manager.delete(self.incident_id, self.id) 74 | 75 | 76 | class IncidentUpdatesManager(Manager): 77 | resource_class = IncidentUpdate 78 | path = "incidents/{}/updates" 79 | 80 | def create(self, *, incident_id: int, status: int, message: str) -> IncidentUpdate: 81 | """ 82 | Create an incident update 83 | 84 | Keyword Args: 85 | incident_id (int): The incident to update 86 | status (int): New status id 87 | message (str): Update message 88 | 89 | Returns: 90 | :py:data:`IncidentUpdate` instance 91 | """ 92 | return self._create( 93 | self.path.format(incident_id), 94 | { 95 | "status": status, 96 | "message": message, 97 | }, 98 | ) 99 | 100 | def update( 101 | self, 102 | *, 103 | id: int, 104 | incident_id: int, 105 | status: int = None, 106 | message: str = None, 107 | **kwargs 108 | ) -> IncidentUpdate: 109 | """ 110 | Update an incident update 111 | 112 | Args: 113 | incident_id (int): The incident 114 | id (int): The incident update id to update 115 | 116 | Keyword Args: 117 | status (int): New status id 118 | message (str): New message 119 | 120 | Returns: 121 | The updated :py:data:`IncidentUpdate` instance 122 | """ 123 | # TODO: Documentation claims data is set as query params 124 | return self._update( 125 | self.path.format(incident_id), 126 | id, 127 | { 128 | "status": status, 129 | "message": message, 130 | }, 131 | ) 132 | 133 | def count(self, incident_id) -> int: 134 | """ 135 | Count the number of incident update for an incident 136 | 137 | Args: 138 | incident_id (int): The incident 139 | 140 | Returns: 141 | int: Number of incident updates for the incident 142 | """ 143 | return self._count(self.path.format(incident_id)) 144 | 145 | def list( 146 | self, incident_id: int, page: int = 1, per_page: int = 20 147 | ) -> Generator[IncidentUpdate, None, None]: 148 | """ 149 | List updates for an issue 150 | 151 | Args: 152 | incident_id: The incident to list updates 153 | 154 | Keyword Args: 155 | page (int): The first page to request 156 | per_page (int): Entries per page 157 | 158 | Return: 159 | Generator of :py:data:`IncidentUpdate`s 160 | """ 161 | return self._list_paginated( 162 | self.path.format(incident_id), 163 | page=page, 164 | per_page=per_page, 165 | ) 166 | 167 | def get(self, incident_id: int, update_id: int) -> IncidentUpdate: 168 | """ 169 | Get an incident update 170 | 171 | Args: 172 | incident_id (int): The incident 173 | update_id (int): The indicent update id to obtain 174 | 175 | Returns: 176 | :py:data:`IncidentUpdate` instance 177 | """ 178 | return self._get(self.path.format(incident_id), update_id) 179 | 180 | def delete(self, incident_id: int, update_id: int) -> None: 181 | """ 182 | Delete an incident update 183 | """ 184 | self._delete(self.path.format(incident_id), update_id) 185 | -------------------------------------------------------------------------------- /cachetclient/v1/metrics.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator, Optional 3 | 4 | from cachetclient.base import Manager, Resource 5 | from cachetclient import utils 6 | from cachetclient.v1.metric_points import MetricPointsManager 7 | from cachetclient.httpclient import HttpClient 8 | 9 | 10 | class Metric(Resource): 11 | @property 12 | def id(self) -> int: 13 | return self.get("id") 14 | 15 | @property 16 | def name(self) -> str: 17 | return self.get("name") 18 | 19 | @name.setter 20 | def name(self, value: str): 21 | self._data["name"] = value 22 | 23 | @property 24 | def suffix(self) -> str: 25 | return self.get("suffix") 26 | 27 | @suffix.setter 28 | def suffix(self, value: str): 29 | self._data["suffix"] = value 30 | 31 | @property 32 | def description(self): 33 | return self.get("description") 34 | 35 | @description.setter 36 | def description(self, value: str): 37 | self._data["description"] = value 38 | 39 | @property 40 | def calc_type(self): 41 | return self.get("calc_type") 42 | 43 | @calc_type.setter 44 | def calc_type(self, value: int): 45 | self._data["calc_type"] = value 46 | 47 | @property 48 | def default_value(self): 49 | return self.get("default_value") 50 | 51 | @default_value.setter 52 | def default_value(self, value: int): 53 | self._data["default_value"] = value 54 | 55 | @property 56 | def display_chart(self) -> int: 57 | return self.get("display_chart") 58 | 59 | @display_chart.setter 60 | def display_chart(self, value: int): 61 | self._data["display_chart"] = value 62 | 63 | @property 64 | def created_at(self) -> Optional[datetime]: 65 | """datetime: When the issue was created""" 66 | return utils.to_datetime(self.get("created_at")) 67 | 68 | @property 69 | def updated_at(self) -> Optional[datetime]: 70 | """datetime: Last time the issue was updated""" 71 | return utils.to_datetime(self.get("updated_at")) 72 | 73 | @property 74 | def places(self) -> int: 75 | return self.get("places") 76 | 77 | @places.setter 78 | def places(self, value: int): 79 | self._data["places"] = value 80 | 81 | @property 82 | def default_view(self) -> int: 83 | return self.get("default_view") 84 | 85 | @default_view.setter 86 | def default_view(self, value: int): 87 | self._data["default_view"] = value 88 | 89 | @property 90 | def threshold(self) -> int: 91 | return self.get("threshold") 92 | 93 | @threshold.setter 94 | def threshold(self, value: int): 95 | self._data["threshold"] = value 96 | 97 | @property 98 | def order(self) -> int: 99 | return self.get("order") 100 | 101 | @order.setter 102 | def order(self, value: int): 103 | self._data["order"] = value 104 | 105 | @property 106 | def visible(self) -> int: 107 | return self.get("visible") 108 | 109 | @visible.setter 110 | def visible(self, value: int): 111 | self._data["visible"] = value 112 | 113 | def points(self) -> Generator["Metric", None, None]: 114 | """Generator['Metric', None, None]: Metric points for this metric""" 115 | return self._manager.points.list(self.id) 116 | 117 | 118 | class MetricsManager(Manager): 119 | resource_class = Metric 120 | path = "metrics" 121 | 122 | def __init__( 123 | self, http_client: HttpClient, metric_update_manager: MetricPointsManager 124 | ): 125 | super().__init__(http_client) 126 | self.points = metric_update_manager 127 | 128 | def create( 129 | self, 130 | *, 131 | name: str, 132 | description: str, 133 | suffix: str, 134 | default_value: int = 0, 135 | display_chart: int = 0 136 | ) -> Metric: 137 | """ 138 | Create a metric. 139 | 140 | Keyword Args: 141 | name (str): Name/title of the metric 142 | description (str): Description of what the metric is measuring 143 | suffix (str): Measurments in 144 | default_value (int): The default value to use when a point is added 145 | display_chart (int): Whether to display the chart on the status page 146 | 147 | Returns: 148 | :py:data:`Metric` instance 149 | """ 150 | return self._create( 151 | self.path, 152 | { 153 | "name": name, 154 | "description": description, 155 | "suffix": suffix, 156 | "default_value": default_value, 157 | "display_chart": display_chart, 158 | }, 159 | ) 160 | 161 | def list(self, page: int = 1, per_page: int = 1) -> Generator[Metric, None, None]: 162 | """ 163 | List all metrics paginated 164 | 165 | Keyword Args: 166 | page (int): Page to start on 167 | per_page (int): entries per page 168 | 169 | Returns: 170 | Generator of :py:data:`Metric`s 171 | """ 172 | return self._list_paginated( 173 | self.path, 174 | page=page, 175 | per_page=per_page, 176 | ) 177 | 178 | def count(self) -> int: 179 | """ 180 | Count the number of metrics 181 | 182 | Returns: 183 | int: Total number of metrics 184 | """ 185 | return self._count(self.path) 186 | 187 | def get(self, metric_id: int) -> Metric: 188 | """ 189 | Get a signle metric 190 | 191 | Args: 192 | metric_id (int): The metric id to get 193 | 194 | Returns: 195 | :py:data:`Metric` instance 196 | 197 | Raises: 198 | :py:data:`requests.exception.HttpError`: if metric do not exist 199 | """ 200 | return self._get(self.path, metric_id) 201 | 202 | def delete(self, metric_id: int) -> None: 203 | """ 204 | Delete an metric 205 | 206 | Args: 207 | metric_id (int): The metric id 208 | """ 209 | self._delete(self.path, metric_id) 210 | -------------------------------------------------------------------------------- /cachetclient/v1/component_groups.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator, List, Optional 3 | 4 | from cachetclient.base import Manager, Resource 5 | from cachetclient import utils 6 | from cachetclient.v1 import enums 7 | from cachetclient.v1.components import Component, ComponentManager 8 | from cachetclient.httpclient import HttpClient 9 | 10 | 11 | class ComponentGroup(Resource): 12 | @property 13 | def id(self) -> int: 14 | """int: Id of the component group""" 15 | return self.get("id") 16 | 17 | @property 18 | def name(self) -> str: 19 | """str: Set or get name of component group""" 20 | return self._data["name"] 21 | 22 | @name.setter 23 | def name(self, value: str): 24 | self._data["name"] = value 25 | 26 | @property 27 | def enabled_components(self) -> List[Component]: 28 | """List[Component]: Enabled components in this group""" 29 | return [ 30 | Component(self._manager.components, comp) 31 | for comp in self._data["enabled_components"] 32 | ] 33 | 34 | @property 35 | def order(self) -> int: 36 | """int: Get or set order value for group""" 37 | return self.get("order") 38 | 39 | @order.setter 40 | def order(self, value: int): 41 | self._data["order"] = value 42 | 43 | @property 44 | def collapsed(self) -> int: 45 | """int: Get or set collapsed status. 46 | See :py:data:`enums` module for values. 47 | """ 48 | return self.get("collapsed") 49 | 50 | @collapsed.setter 51 | def collapsed(self, value): 52 | self._data["collapsed"] = value 53 | 54 | @property 55 | def lowest_human_status(self): 56 | """str: Lowest component status, human readable""" 57 | return self.get("lowest_human_status") 58 | 59 | @property 60 | def is_collapsed(self) -> bool: 61 | """bool: Does the current collapsed value indicate the group is collapsed? 62 | Note that the collapsed value may also indicate the group is not operational. 63 | """ 64 | return self.collapsed == enums.COMPONENT_GROUP_COLLAPSED_TRUE 65 | 66 | @property 67 | def is_open(self) -> bool: 68 | """bool: Does the current collapsed value indicate the group is open? 69 | Note that the collapsed value may also indicate the group is not operational. 70 | """ 71 | return self.collapsed == enums.COMPONENT_GROUP_COLLAPSED_FALSE 72 | 73 | @property 74 | def is_operational(self) -> bool: 75 | """bool: Does the current collapsed value indicate the group not operational?""" 76 | return self.collapsed != enums.COMPONENT_GROUP_COLLAPSED_NOT_OPERATIONAL 77 | 78 | @property 79 | def created_at(self) -> Optional[datetime]: 80 | """datetime: When the group was created""" 81 | return utils.to_datetime(self.get("created_at")) 82 | 83 | @property 84 | def updated_at(self) -> Optional[datetime]: 85 | """datetime: Last time updated""" 86 | return utils.to_datetime(self.get("updated_at")) 87 | 88 | @property 89 | def visible(self) -> bool: 90 | """bool: Get or set visibility of the group""" 91 | return self.get("visible") == 1 92 | 93 | @visible.setter 94 | def visible(self, value: bool): 95 | self._data["visible"] = value 96 | 97 | 98 | class ComponentGroupManager(Manager): 99 | resource_class = ComponentGroup 100 | path = "components/groups" 101 | 102 | def __init__(self, http_client: HttpClient, components_manager: ComponentManager): 103 | super().__init__(http_client) 104 | self.components = components_manager 105 | 106 | def create( 107 | self, *, name: str, order: int = 0, collapsed: int = 0, visible: bool = False 108 | ) -> ComponentGroup: 109 | """ 110 | Create a component group 111 | 112 | Keyword Args: 113 | name (str): Name of the group 114 | order (int): group order 115 | collapsed (int): Collapse value (see enums) 116 | visible (bool): Publicly visible group 117 | 118 | Returns: 119 | :py:data:`ComponentGroup` instance 120 | """ 121 | return self._create( 122 | self.path, 123 | { 124 | "name": name, 125 | "order": order, 126 | "collapsed": collapsed, 127 | "visible": 1 if visible else 0, 128 | }, 129 | ) 130 | 131 | def update( 132 | self, 133 | group_id: int, 134 | *, 135 | name: str, 136 | order: int = None, 137 | collapsed: int = None, 138 | visible: bool = None, 139 | **kwargs 140 | ) -> ComponentGroup: 141 | """ 142 | Update component group 143 | 144 | Args: 145 | group_id (int): The group id to update 146 | 147 | Keyword Args: 148 | name (str): New name for group 149 | order (int): Order value of the group 150 | collapsed (int): Collapsed value. See enums module. 151 | visible (bool): Publicly visible group 152 | """ 153 | return self._update( 154 | self.path, 155 | group_id, 156 | self._build_data_dict( 157 | name=name, 158 | order=order, 159 | collapsed=collapsed, 160 | visible=1 if visible else 0, 161 | ), 162 | ) 163 | 164 | def count(self) -> int: 165 | """ 166 | Count the number of component groups 167 | 168 | Returns: 169 | int: Number of component groups 170 | """ 171 | return self._count(self.path) 172 | 173 | def list( 174 | self, page: int = 1, per_page: int = 20 175 | ) -> Generator[ComponentGroup, None, None]: 176 | """ 177 | List all component groups 178 | 179 | Keyword Args: 180 | page (int): The page to start listing 181 | per_page: Number of entries per page 182 | 183 | Returns: 184 | Generator of :py:data:`ComponentGroup` instances 185 | """ 186 | yield from self._list_paginated(self.path, page=page, per_page=per_page) 187 | 188 | def get(self, group_id) -> ComponentGroup: 189 | """ 190 | Get a component group by id 191 | 192 | Args: 193 | group_id (int): Id of the component group 194 | 195 | Returns: 196 | :py:data:`ComponentGroup` instance 197 | 198 | Raises: 199 | `requests.exceptions.HttpError`: if not found 200 | """ 201 | return self._get(self.path, group_id) 202 | 203 | def delete(self, group_id: int) -> None: 204 | """ 205 | Delete a component group 206 | 207 | Args: 208 | group_id (int): Id of the component 209 | 210 | Raises: 211 | `requests.exceptions.HttpError`: if not found 212 | """ 213 | self._delete(self.path, group_id) 214 | -------------------------------------------------------------------------------- /cachetclient/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Generator, Optional, List 3 | 4 | from cachetclient.httpclient import HttpClient 5 | 6 | 7 | class Resource: 8 | """Bag of attributes""" 9 | 10 | def __init__(self, manager, data): 11 | """Resource initializer. 12 | 13 | Args: 14 | manager: The manager this resource belongs to 15 | data: The raw json data 16 | """ 17 | self._manager = manager 18 | self._data = data 19 | 20 | @property 21 | def attrs(self) -> dict: 22 | """dict: The raw json response from the server""" 23 | return self._data 24 | 25 | def get(self, name) -> Any: 26 | """ 27 | Safely obtain any attribute name for the resource 28 | 29 | Args: 30 | name (str): Key name in json response 31 | 32 | Returns: 33 | Value from the raw json response. 34 | If the key doesn't exist ``None`` is returned. 35 | """ 36 | return self._data.get(name) 37 | 38 | def update(self): 39 | """ 40 | Posts the values in the resource to the server. 41 | 42 | Example:: 43 | 44 | # Change an attribute and save the resource 45 | >> resource.value = something 46 | >> updated_resource = resource.update() 47 | 48 | Returns: 49 | Resource: The updated resource from the server 50 | """ 51 | return self._manager.update(self.get("id"), **self.attrs) 52 | 53 | def delete(self) -> None: 54 | """ 55 | Deletes the resource from the server. 56 | 57 | Raises: 58 | HTTPException if the resource don't exist. 59 | """ 60 | self._manager.delete(self.get("id")) 61 | 62 | def __repr__(self) -> str: 63 | return str(self) 64 | 65 | def __str__(self) -> str: 66 | return str(self._data) 67 | 68 | 69 | class Manager: 70 | """ 71 | Base class for handling crud resources 72 | """ 73 | 74 | resource_class = Resource 75 | path: Optional[str] = None 76 | 77 | def __init__(self, http_client: HttpClient): 78 | """Manager initializer. 79 | 80 | Args: 81 | http_client: The httpclient 82 | """ 83 | self._http = http_client 84 | 85 | if self.resource_class is None: 86 | raise ValueError( 87 | "resource_class not defined in class {}".format(self.__class__) 88 | ) 89 | 90 | # if self.path is None: 91 | # raise ValueError("path not defined for class {}".format(self.__class__)) 92 | 93 | def instance_from_dict(self, data: dict) -> Resource: 94 | """Creates a resource instance from a dictionary. 95 | 96 | This doesn't hit any endpoints in cachet, but rather 97 | enables us to create a resource class instance from 98 | dictionary data. This can be useful when caching 99 | data from cachet in memcache or databases. 100 | 101 | Args: 102 | data (dict): dictionary containing the instance data 103 | Returns: 104 | Resource: The resource class instance 105 | """ 106 | return self.resource_class(self, data) 107 | 108 | def instance_from_json(self, data: str) -> Resource: 109 | """Creates a resource instance from a json string. 110 | 111 | This doesn't hit any endpoints in cachet, but rather 112 | enables us to create a resource class instance from 113 | json data. This can be useful when caching 114 | data from cachet in memcache or databases. 115 | 116 | Args: 117 | data (str): json string containing the instance data 118 | Returns: 119 | Resource: The resource class instance 120 | """ 121 | return self.resource_class(self, json.loads(data)) 122 | 123 | def instance_list_from_json(self, data: str) -> List[Resource]: 124 | """Creates a resource instance list from a json string. 125 | 126 | This doesn't hit any endpoints in cachet, but rather 127 | enables us to create a resource class instances from 128 | json data. This can be useful when caching 129 | data from cachet in memcache or databases. 130 | 131 | Args: 132 | data (str): json string containing the instance data 133 | Returns: 134 | Resource: The resource class instance 135 | Raises: 136 | ValueError: if json data do not deserialize into a list 137 | """ 138 | instances = json.loads(data) 139 | if not isinstance(instances, list): 140 | raise ValueError( 141 | "json data is {}, not a list : {}".format(type(instances), instances) 142 | ) 143 | 144 | return [self.resource_class(self, inst) for inst in instances] 145 | 146 | def _create(self, path: str, data: dict): 147 | response = self._http.post(path, data=data) 148 | return self.resource_class(self, response.json()["data"]) 149 | 150 | def _update(self, path: str, resource_id: int, data: dict) -> Resource: 151 | """Generic resource updater 152 | 153 | Args: 154 | path (str): url path relative to base url 155 | resource_id (int): The resource to update 156 | data (dict): New data 157 | 158 | Returns: 159 | Resource: The updated resource from the server 160 | """ 161 | response = self._http.put("{}/{}".format(path, resource_id), data=data) 162 | return self.resource_class(self, response.json()["data"]) 163 | 164 | def _list_paginated( 165 | self, path: str, page=1, per_page=20 166 | ) -> Generator[Resource, None, None]: 167 | """List resources paginated. 168 | 169 | Args: 170 | path (str): url path relative to base url 171 | 172 | Keyword Args: 173 | page (int): Page to start on 174 | per_page (int): Number of entries per page 175 | 176 | Returns: 177 | Generator of resources 178 | """ 179 | while True: 180 | result = self._http.get( 181 | path, 182 | params={ 183 | "page": page, 184 | "per_page": per_page, 185 | }, 186 | ) 187 | json_data = result.json() 188 | 189 | meta = json_data["meta"] 190 | data = json_data["data"] 191 | 192 | for entry in data: 193 | yield self.resource_class(self, entry) 194 | 195 | if page >= meta["pagination"]["total_pages"]: 196 | break 197 | 198 | page += 1 199 | 200 | # def _search(self, path, params=None): 201 | # params = params or {} 202 | # result = self._http.get(path, params={'per_page': 1, **params}) 203 | # json_data = result.json() 204 | 205 | def _get(self, path: str, resource_id: int): 206 | """Generic resource getter (single) 207 | 208 | Args: 209 | path (str): url path relative to base url 210 | resource_id (int): The resource id to get 211 | 212 | Returns: 213 | :py:data:`Resource`: A resource instance 214 | """ 215 | result = self._http.get("{}/{}".format(path, resource_id)) 216 | json_data = result.json() 217 | return self.resource_class(self, json_data["data"]) 218 | 219 | def _count(self, path: str) -> int: 220 | """Generic count method 221 | using the pagination system to obtain the total number of resources 222 | 223 | Args: 224 | path (str): url path relative to base url 225 | 226 | Returns: 227 | int: Number of resources 228 | """ 229 | result = self._http.get(path, params={"per_page": 1}) 230 | json_data = result.json() 231 | return json_data["meta"]["pagination"]["total"] 232 | 233 | def _delete(self, path: str, resource_id: int) -> None: 234 | """Generic resource deleter 235 | 236 | Args: 237 | path (str): url path relative to base url 238 | resource_id (int): The resource to delete 239 | """ 240 | self._http.delete(path, resource_id) 241 | 242 | def _build_data_dict(self, **kwargs) -> dict: 243 | """Builds a data dictionary for posting to the server. 244 | 245 | Will omit key/value pars with None values. 246 | This makes partial updates less error prone. 247 | 248 | Returns: 249 | dict: dict without `None` values 250 | """ 251 | return {key: value for key, value in kwargs.items() if value is not None} 252 | -------------------------------------------------------------------------------- /cachetclient/v1/incidents.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import datetime 3 | from typing import List, Generator, Optional 4 | 5 | from cachetclient.base import Manager, Resource 6 | from cachetclient import utils 7 | from cachetclient.v1.incident_updates import IncidentUpdatesManager 8 | from cachetclient.httpclient import HttpClient 9 | 10 | 11 | class Incident(Resource): 12 | @property 13 | def id(self) -> int: 14 | """int: unique id of the incident""" 15 | return self.get("id") 16 | 17 | @property 18 | def component_id(self) -> int: 19 | """int: Get or set component id for this incident""" 20 | return self.get("component_id") 21 | 22 | @component_id.setter 23 | def component_id(self, value: int): 24 | self._data["component_id"] = value 25 | 26 | @property 27 | def name(self) -> str: 28 | """str: Get or set name/title of the incident""" 29 | return self.get("name") 30 | 31 | @name.setter 32 | def name(self, value: str): 33 | self._data["name"] = value 34 | 35 | @property 36 | def message(self) -> str: 37 | """str: Get or set message""" 38 | return self.get("message") 39 | 40 | @message.setter 41 | def message(self, value: str): 42 | self._data["message"] = value 43 | 44 | @property 45 | def notify(self) -> str: 46 | """bool: Get or set notification flag""" 47 | return self.get("notify") 48 | 49 | @notify.setter 50 | def notify(self, value: bool): 51 | self._data["notify"] = value 52 | 53 | @property 54 | def status(self) -> int: 55 | """int: Get or set status. See :py:data:`enums`""" 56 | return self.get("status") 57 | 58 | @status.setter 59 | def status(self, value: int): 60 | self._data["status"] = value 61 | 62 | @property 63 | def human_status(self) -> str: 64 | """str: Human representation of the status""" 65 | return self.get("human_status") 66 | 67 | @property 68 | def visible(self) -> int: 69 | """bool: Get or set visibility of the incident""" 70 | return self.get("visible") == 1 71 | 72 | @visible.setter 73 | def visible(self, value: bool): 74 | self._data["visible"] = value 75 | 76 | @property 77 | def stickied(self) -> int: 78 | """bool: Get or set sticky value of the incident (cachet 2.4)""" 79 | return self.get("stickied") == 1 80 | 81 | @stickied.setter 82 | def stickied(self, value: bool): 83 | self._data["stickied"] = value 84 | 85 | @property 86 | def scheduled_at(self) -> Optional[datetime]: 87 | """datetime: Scheduled time. This is used for scheduled events 88 | like maintenance in Cachet 2.3 were incident status is ``INCIDENT_SCHEDULED``. 89 | 2.4 has its own schedule resource and endpoints. 90 | """ 91 | return utils.to_datetime(self.get("scheduled_at")) 92 | 93 | @property 94 | def occurred_at(self) -> Optional[datetime]: 95 | """datetime: When the issue was occurred""" 96 | return utils.to_datetime(self.get("occurred_at")) 97 | 98 | @property 99 | def created_at(self) -> Optional[datetime]: 100 | """datetime: When the issue was created""" 101 | return utils.to_datetime(self.get("created_at")) 102 | 103 | @property 104 | def updated_at(self) -> Optional[datetime]: 105 | """datetime: Last time the issue was updated""" 106 | return utils.to_datetime(self.get("updated_at")) 107 | 108 | @property 109 | def deleted_at(self) -> Optional[datetime]: 110 | """datetime: When the issue was deleted""" 111 | return utils.to_datetime(self.get("deleted_at")) 112 | 113 | def updates(self) -> Generator["Incident", None, None]: 114 | """Generator['Incident', None, None]: Incident updates for this issue""" 115 | return self._manager.updates.list(self.id) 116 | 117 | def update(self): 118 | """ 119 | Posts the values in the resource to the server. 120 | 121 | Example:: 122 | 123 | # Change an attribute and save the resource 124 | >> resource.value = something 125 | >> updated_resource = resource.update() 126 | 127 | Returns: 128 | The updated resource from the server 129 | """ 130 | # Convert date strings to datetime 131 | data = copy.deepcopy(self.attrs) 132 | data["created_at"] = self.created_at 133 | data["occurred_at"] = self.occurred_at 134 | return self._manager.update(self.get("id"), **data) 135 | 136 | 137 | class IncidentManager(Manager): 138 | resource_class = Incident 139 | path = "incidents" 140 | 141 | def __init__( 142 | self, http_client: HttpClient, incident_update_manager: IncidentUpdatesManager 143 | ): 144 | super().__init__(http_client) 145 | self.updates = incident_update_manager 146 | 147 | def create( 148 | self, 149 | *, 150 | name: str, 151 | message: str, 152 | status: int, 153 | visible: bool = True, 154 | stickied: bool = False, 155 | component_id: int = None, 156 | component_status: int = None, 157 | notify: bool = True, 158 | created_at: datetime = None, 159 | occurred_at: datetime = None, 160 | template: str = None, 161 | template_vars: List[str] = None 162 | ) -> Incident: 163 | """ 164 | Create and general issue or issue for a component. 165 | component_id and component_status must be supplied when making 166 | a component issue. 167 | 168 | Keyword Args: 169 | name (str): Name/title of the issue 170 | message (str): Mesage body for the issue 171 | status (int): Status of the incident (see enums) 172 | visible (bool): Publicly visible incident 173 | stickied (bool): Stickied incident 174 | component_id (int): The component to update 175 | component_status (int): The status to apply on component 176 | notify (bool): If users should be notified 177 | occurred_at: when the incident occurred (cachet 2.4) 178 | created_at: when the incident was created (cachet 2.3) 179 | template (str): Slug of template to use 180 | template_vars (list): Variables to the template 181 | 182 | Returns: 183 | Incident instance 184 | """ 185 | is_component_update = component_id is not None and component_status is not None 186 | 187 | return self._create( 188 | self.path, 189 | self._build_data_dict( 190 | name=name, 191 | message=message, 192 | status=status, 193 | visible=1 if visible else 0, 194 | stickied=1 if stickied else 0, 195 | component_id=component_id if is_component_update else None, 196 | component_status=component_status if is_component_update else None, 197 | notify=1 if notify else 0, 198 | created_at=created_at.strftime("%Y-%m-%d %H:%M:%S") 199 | if created_at 200 | else None, 201 | occurred_at=occurred_at.strftime("%Y-%m-%d %H:%M:%S") 202 | if occurred_at 203 | else None, 204 | template=template, 205 | vars=template_vars or [], 206 | ), 207 | ) 208 | 209 | def update( 210 | self, 211 | incident_id: int, 212 | name: str = None, 213 | message: str = None, 214 | status: int = None, 215 | visible: bool = None, 216 | stickied: bool = False, 217 | component_id: int = None, 218 | component_status: int = None, 219 | notify: bool = True, 220 | occurred_at: datetime = None, 221 | template: str = None, 222 | template_vars: List[str] = None, 223 | **kwargs 224 | ) -> Incident: 225 | """ 226 | Update an incident. 227 | 228 | Args: 229 | incident_id (int): The incident to update 230 | 231 | Keyword Args: 232 | name (str): Name/title of the issue 233 | message (str): Mesage body for the issue 234 | status (int): Status of the incident (see enums) 235 | visible (bool): Publicly visible incident 236 | stickied (bool): Stickied incident 237 | component_id (int): The component to update 238 | component_status (int): The status to apply on component 239 | notify (bool): If users should be notified 240 | occurred_at (datetime): when the incident was occurred 241 | template (str): Slug of template to use 242 | template_vars (list): Variables to the template 243 | 244 | Returns: 245 | Updated incident Instance 246 | """ 247 | if name is None or message is None or status is None or visible is None: 248 | raise ValueError( 249 | "name, message, status and visible are required parameters" 250 | ) 251 | 252 | is_component_update = component_id is not None and component_status is not None 253 | 254 | return self._update( 255 | self.path, 256 | incident_id, 257 | self._build_data_dict( 258 | name=name, 259 | message=message, 260 | status=status, 261 | visible=1 if visible else 0, 262 | stickied=1 if stickied else 0, 263 | component_id=component_id if is_component_update else None, 264 | component_status=component_status if is_component_update else None, 265 | notify=1 if notify else 0, 266 | occurred_at=occurred_at.strftime("%Y-%m-%d %H:%M") 267 | if occurred_at 268 | else None, 269 | template=template, 270 | vars=template_vars, 271 | ), 272 | ) 273 | 274 | def list(self, page: int = 1, per_page: int = 1) -> Generator[Incident, None, None]: 275 | """ 276 | List all incidents paginated 277 | 278 | Keyword Args: 279 | page (int): Page to start on 280 | per_page (int): entries per page 281 | 282 | Returns: 283 | Generator of :py:data:`Incident`s 284 | """ 285 | return self._list_paginated( 286 | self.path, 287 | page=page, 288 | per_page=per_page, 289 | ) 290 | 291 | def get(self, incident_id: int) -> Incident: 292 | """ 293 | Get a single incident 294 | 295 | Args: 296 | incident_id (int): The incident id to get 297 | 298 | Returns: 299 | :py:data:`Incident` instance 300 | 301 | Raises: 302 | :py:data:`requests.exception.HttpError`: if incident do not exist 303 | """ 304 | return self._get(self.path, incident_id) 305 | 306 | def count(self) -> int: 307 | """ 308 | Count the number of incidents 309 | 310 | Returns: 311 | int: Total number of incidents 312 | """ 313 | return self._count(self.path) 314 | 315 | def delete(self, incident_id: int) -> None: 316 | """ 317 | Delete an incident 318 | 319 | Args: 320 | incident_id (int): The incident id 321 | """ 322 | self._delete(self.path, incident_id) 323 | -------------------------------------------------------------------------------- /extras/live_run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run some quick simple tests on an actual cachet setup. 3 | This modules is not pretty, but gets the work done. 4 | This can be set up locally with docker fairly quickly. 5 | 6 | Set the following environment variables before running the script: 7 | 8 | - CACHET_ENDPOINT (eg: http://localhost:8000/api/v1) 9 | - CACHET_API_TOKEN (eg. Wohc7eeGhaewae7zie1E) 10 | """ 11 | import os 12 | import sys 13 | import traceback 14 | from datetime import datetime 15 | from pprint import pprint 16 | 17 | from requests.exceptions import HTTPError 18 | 19 | import cachetclient 20 | from cachetclient.v1.client import Client 21 | from cachetclient.v1 import enums 22 | 23 | import logging 24 | logger = logging.getLogger('cachetclient') 25 | logger.setLevel(logging.DEBUG) 26 | 27 | # Initial component group we use for testing 28 | DEFAULT_COMPONENT_GROUP = 1 29 | 30 | CACHET_ENDPOINT = os.environ.get('CACHET_ENDPOINT') 31 | CACHET_API_TOKEN = os.environ.get('CACHET_API_TOKEN') 32 | CLIENT = None 33 | 34 | 35 | class Stats: 36 | """Basic stats for tests""" 37 | NUM_TESTS = 0 38 | NUM_TESTS_SUCCESS = 0 39 | NUM_TESTS_FAIL = 0 40 | 41 | @classmethod 42 | def incr_tests(cls): 43 | cls.NUM_TESTS += 1 44 | 45 | @classmethod 46 | def incr_success(cls): 47 | cls.NUM_TESTS_SUCCESS += 1 48 | 49 | @classmethod 50 | def incr_fail(cls): 51 | cls.NUM_TESTS_FAIL += 1 52 | 53 | @classmethod 54 | def success_percentage(cls): 55 | if cls.NUM_TESTS == 0: 56 | return 0.0 57 | 58 | return round(cls.NUM_TESTS_SUCCESS * 100 / cls.NUM_TESTS, 2) 59 | 60 | 61 | def client() -> Client: 62 | global CLIENT 63 | if CLIENT is None: 64 | CLIENT = cachetclient.Client(endpoint=CACHET_ENDPOINT, api_token=CACHET_API_TOKEN) 65 | 66 | return CLIENT 67 | 68 | 69 | def simple_test(halt_on_exception=False): 70 | """Simple decorator for handling test functions""" 71 | def decorator_func(func): 72 | def wrapper(*args, **kwargs): 73 | Stats.incr_tests() 74 | print(func.__name__) 75 | print("-" * 80) 76 | try: 77 | func(*args, **kwargs) 78 | Stats.incr_success() 79 | print() 80 | except AssertionError as ex: 81 | Stats.incr_fail() 82 | _, _, tb = sys.exc_info() 83 | traceback.print_tb(tb) # Fixed format 84 | tb_info = traceback.extract_tb(tb) 85 | filename, line, function, text = tb_info[-1] 86 | print("### EXCEPTION ###") 87 | print('An error occurred on line {} in statement {}'.format(line, text)) 88 | print(ex) 89 | print() 90 | except Exception as ex: 91 | Stats.incr_fail() 92 | print("### EXCEPTION ###") 93 | print(ex) 94 | print() 95 | finally: 96 | if halt_on_exception: 97 | raise 98 | 99 | return wrapper 100 | return decorator_func 101 | 102 | 103 | def main(): 104 | if CACHET_ENDPOINT is None: 105 | raise ValueError("CACHET_ENDPOINT environment variable missing") 106 | 107 | if CACHET_API_TOKEN is None: 108 | raise ValueError("CACHET_API_TOKEN environment variable missing") 109 | 110 | setup() 111 | version = client().version() 112 | 113 | # Version 2.3.x features 114 | test_ping() 115 | test_version() 116 | test_components() 117 | test_component_groups() 118 | test_subscribers() 119 | test_incidents() 120 | test_metrics() 121 | test_metric_points() 122 | 123 | # Version 2.4.x feature 124 | if version.value.startswith("2.4"): 125 | # test_incident_updates() 126 | test_schedules() 127 | 128 | print("=" * 80) 129 | print("Number of tests :", Stats.NUM_TESTS) 130 | print("Successful :", Stats.NUM_TESTS_SUCCESS) 131 | print("Failure :", Stats.NUM_TESTS_FAIL) 132 | print("Percentage passed : {}%".format(Stats.success_percentage())) 133 | print("=" * 80) 134 | 135 | 136 | def setup(): 137 | # Get the default component 138 | try: 139 | client().components.get(DEFAULT_COMPONENT_GROUP) 140 | except HTTPError: 141 | print("Cannot find default component group. Creating.") 142 | client().components.create( 143 | name="Generic Component", 144 | status=enums.COMPONENT_STATUS_OPERATIONAL, 145 | description="Generic component group for live testing", 146 | ) 147 | 148 | 149 | @simple_test() 150 | def test_ping(): 151 | result = client().ping() 152 | if result is not True: 153 | raise ValueError("Ping failed. {} ({}) returned instead of True (bool)".format(result, type(result))) 154 | 155 | 156 | @simple_test() 157 | def test_version(): 158 | version = client().version() 159 | if version.value is not str and len(version.value) < 3: 160 | raise ValueError("Version value string suspicious? '{}'".format(version.value)) 161 | 162 | print("Version :", version.value) 163 | print("on_latest :", version.on_latest) 164 | print("latest :", version.latest) 165 | 166 | 167 | @simple_test() 168 | def test_components(): 169 | comp = client().components.create( 170 | name="Test Component", 171 | status=enums.COMPONENT_STATUS_OPERATIONAL, 172 | description="This is a test", 173 | tags=("Test Tag", "Another Test Tag"), 174 | order=1, 175 | group_id=1, 176 | ) 177 | pprint(comp.attrs, indent=2) 178 | assert comp.status == enums.COMPONENT_STATUS_OPERATIONAL, "Incorrect status" 179 | assert isinstance(comp.created_at, datetime), "created_at not datetime" 180 | assert isinstance(comp.updated_at, datetime), "updated_at not datetime" 181 | assert comp.has_tag(name="Test Tag") 182 | assert comp.has_tag(name="Another Test Tag") 183 | assert comp.has_tag(slug="Test-Tag") 184 | assert comp.has_tag(slug="Another-Test-Tag") 185 | 186 | # Create component using properties 187 | comp.name = 'Test Thing' 188 | comp.status = enums.COMPONENT_STATUS_MAJOR_OUTAGE 189 | comp.link = 'http://status.example.com' 190 | comp.order = 10 191 | comp.group_id = 1000 192 | comp.enabled = False 193 | comp.set_tags(("Updated Tag 1", "Updated Tag 2")) 194 | comp = comp.update() 195 | pprint(comp.attrs, indent=2) 196 | 197 | # Test if values are correctly updates 198 | assert comp.name == 'Test Thing', "Component name differs" 199 | assert comp.description == 'This is a test', "Component description differs" 200 | assert comp.status == enums.COMPONENT_STATUS_MAJOR_OUTAGE, "Component status differs" 201 | assert comp.link == 'http://status.example.com', "Component link differs" 202 | assert comp.order == 10, "Component oder differs" 203 | assert comp.group_id == 1000, "Group id differs" 204 | assert comp.enabled is False, "Component enable status differs" 205 | assert comp.tag_names == ["Updated Tag 1", "Updated Tag 2"], "Tags differs" 206 | 207 | # Call update directly on the manager 208 | comp = client().components.update( 209 | comp.id, 210 | status=enums.COMPONENT_STATUS_OPERATIONAL, 211 | name="A new component name", 212 | tags=("Some Tag", "Another Tag"), 213 | enabled=True, 214 | ) 215 | assert comp.name == "A new component name" 216 | assert comp.description == 'This is a test' 217 | assert comp.status == enums.COMPONENT_STATUS_OPERATIONAL 218 | assert comp.link == 'http://status.example.com' 219 | assert comp.order == 10 220 | assert comp.group_id == 1000 221 | assert comp.enabled is True 222 | assert sorted(comp.tag_names) == ["Another Tag", "Some Tag"] 223 | assert comp.has_tag(name="another Tag") 224 | assert comp.has_tag(name="some Tag") 225 | assert comp.has_tag(slug="some-tag") 226 | assert comp.has_tag(slug="another-tag") 227 | assert comp.has_tag(slug="test") is False 228 | assert comp.has_tag(name="test") is False 229 | comp.delete() 230 | 231 | 232 | @simple_test() 233 | def test_component_groups(): 234 | grp = client().component_groups.create(name="Test Group", order=1) 235 | assert grp.id > 0 236 | assert grp.name == "Test Group" 237 | assert grp.order == 1 238 | assert grp.is_collapsed is False 239 | assert grp.is_open is True 240 | assert grp.is_operational is True 241 | # assert grp.visible is False 242 | 243 | # Re-fetch by id 244 | grp = client().component_groups.get(grp.id) 245 | 246 | # Update group 247 | grp.order = 2 248 | grp.name = "Global Services" 249 | grp.collapsed = enums.COMPONENT_GROUP_COLLAPSED_TRUE 250 | assert grp.id > 0 251 | assert grp.name == "Global Services" 252 | assert grp.order == 2 253 | assert grp.is_collapsed is True 254 | assert grp.is_open is False 255 | assert grp.is_operational is True 256 | # assert grp.visible is False 257 | 258 | pprint(grp.attrs, indent=2) 259 | grp.delete() 260 | 261 | 262 | @simple_test() 263 | def test_subscribers(): 264 | new_sub = client().subscribers.create(email='user@example.test') 265 | 266 | assert isinstance(new_sub.created_at, datetime) 267 | assert isinstance(new_sub.updated_at, datetime) 268 | assert isinstance(new_sub.verified_at, datetime) 269 | 270 | # Rough subscriber count check 271 | count = client().subscribers.count() 272 | if count == 0: 273 | raise ValueError("Subscriber count is 0") 274 | 275 | # Iterate subscribers 276 | for sub in client().subscribers.list(): 277 | print(sub) 278 | 279 | # Delete subscriber and recount 280 | new_sub.delete() 281 | count_pre = client().subscribers.count() 282 | if count != count_pre + 1: 283 | raise ValueError("subscriber count {} != {}".format(count, count_pre)) 284 | 285 | 286 | @simple_test() 287 | def test_incidents(): 288 | issue = client().incidents.create( 289 | name="Something blew up!", 290 | message="We are looking into it", 291 | status=enums.INCIDENT_INVESTIGATING, 292 | component_id=1, 293 | component_status=enums.COMPONENT_STATUS_MAJOR_OUTAGE, 294 | ) 295 | pprint(issue.attrs) 296 | issue = issue.update() 297 | issue.delete() 298 | 299 | 300 | @simple_test() 301 | def test_incident_updates(): 302 | """Requires 2.4""" 303 | incident = client().incidents.create( 304 | "Something blew up!", 305 | "We are looking into it", 306 | enums.INCIDENT_INVESTIGATING, 307 | visible=True, 308 | component_id=1, 309 | component_status=enums.COMPONENT_STATUS_MAJOR_OUTAGE, 310 | ) 311 | 312 | client().incident_updates.create( 313 | incident.id, 314 | enums.INCIDENT_IDENTIFIED, 315 | "We have found the source", 316 | ) 317 | updates = list(incident.updates()) 318 | print("Updates", updates) 319 | incident.delete() 320 | 321 | 322 | @simple_test() 323 | def test_schedules(): 324 | start_time = datetime(2020, 9, 1, 20) 325 | end_time = datetime(2020, 9, 1, 21) 326 | sch = client().schedules.create( 327 | name="Test Schedule", 328 | status=enums.SCHEDULE_STATUS_UPCOMING, 329 | message="Shits gonna happen", 330 | scheduled_at=start_time, 331 | completed_at=end_time, 332 | notify=False, 333 | ) 334 | 335 | sch = client().schedules.get(sch.id) 336 | 337 | assert isinstance(sch.id, int) 338 | assert sch.status == enums.SCHEDULE_STATUS_UPCOMING 339 | assert sch.name == "Test Schedule" 340 | assert sch.message == "Shits gonna happen" 341 | assert sch.scheduled_at == start_time 342 | assert sch.completed_at == end_time 343 | 344 | sch.delete() 345 | 346 | 347 | @simple_test() 348 | def test_metrics(): 349 | print("HELLO") 350 | 351 | 352 | @simple_test() 353 | def test_metric_points(): 354 | pass 355 | 356 | 357 | if __name__ == '__main__': 358 | main() 359 | -------------------------------------------------------------------------------- /cachetclient/v1/components.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import ( 3 | Dict, 4 | Iterable, 5 | Generator, 6 | List, 7 | Optional, 8 | ) 9 | from datetime import datetime 10 | from collections import abc 11 | 12 | from cachetclient.base import Manager, Resource 13 | from cachetclient.v1 import enums 14 | from cachetclient import utils 15 | 16 | 17 | class Component(Resource): 18 | def __init__(self, manager, data): 19 | super().__init__(manager, data) 20 | if data.get("tags") is None: 21 | data["tags"] = {} 22 | 23 | @property 24 | def id(self) -> int: 25 | """int: The unique ID of the component""" 26 | return self._data["id"] 27 | 28 | @property 29 | def name(self) -> str: 30 | """str: Get or set name of the component""" 31 | return self._data["name"] 32 | 33 | @name.setter 34 | def name(self, value: str): 35 | self._data["name"] = value 36 | 37 | @property 38 | def description(self) -> str: 39 | """str: Get or set component description""" 40 | return self.get("description") 41 | 42 | @description.setter 43 | def description(self, value: str): 44 | self._data["description"] = value 45 | 46 | @property 47 | def link(self) -> str: 48 | """str: Get or set http link to the component""" 49 | return self._data["link"] 50 | 51 | @link.setter 52 | def link(self, value: str): 53 | self._data["link"] = value 54 | 55 | @property 56 | def status(self) -> int: 57 | """int: Get or set status id of the component (see :py:data:`enums`)""" 58 | return self._data["status"] 59 | 60 | @status.setter 61 | def status(self, value: int): 62 | self._data["status"] = value 63 | 64 | @property 65 | def status_name(self) -> str: 66 | """str: Human readable status representation""" 67 | return self._data["status_name"] 68 | 69 | @property 70 | def order(self) -> int: 71 | """int: Get or set order of the component in a group""" 72 | return self._data["order"] 73 | 74 | @order.setter 75 | def order(self, value: int): 76 | self._data["order"] = value 77 | 78 | @property 79 | def group_id(self) -> int: 80 | """int: Get or set the component group id""" 81 | return self._data["group_id"] 82 | 83 | @group_id.setter 84 | def group_id(self, value: int): 85 | self._data["group_id"] = value 86 | 87 | @property 88 | def enabled(self) -> bool: 89 | """bool: Get or set enabled state""" 90 | return self._data["enabled"] 91 | 92 | @enabled.setter 93 | def enabled(self, value: bool): 94 | self._data["enabled"] = value 95 | 96 | @property 97 | def tags(self) -> Dict[str, str]: 98 | """dict: Get the raw ``slug: name`` tag dictionary. 99 | 100 | Example:: 101 | 102 | >> component.tags 103 | {'another-test-tag': 'Another Test Tag', 'test-tag': 'Test Tag'} 104 | 105 | Also see :py:data:`add_tag`, :py:data:`add_tags`, :py:data:`set_tags`, 106 | :py:data:`del_tag` and :py:data:`has_tag` methods. 107 | """ 108 | return self._data["tags"] or {} 109 | 110 | @property 111 | def tag_names(self) -> List[str]: 112 | """List[str]: Get the tag names as a list""" 113 | return list(self.tags.values()) 114 | 115 | @property 116 | def tag_slugs(self) -> List[str]: 117 | """List[str]: Get the tag slugs as a list""" 118 | return list(self.tags.keys()) 119 | 120 | @property 121 | def created_at(self) -> Optional[datetime]: 122 | """datetime: When the component was created""" 123 | return utils.to_datetime(self.get("created_at")) 124 | 125 | @property 126 | def updated_at(self) -> Optional[datetime]: 127 | """datetime: Last time the component was updated""" 128 | return utils.to_datetime(self.get("updated_at")) 129 | 130 | def set_tags(self, names: Iterable[str]): 131 | """Replace the current tags. 132 | 133 | .. Note:: We use the provided names also as the slugs. 134 | When the resource is returned from the server the next time 135 | it will be slugified. 136 | """ 137 | self._data["tags"] = {n: n for n in names} 138 | 139 | def add_tags(self, names: Iterable[str]) -> None: 140 | """Add multiple tags. 141 | 142 | .. Note:: We use the provided name also as the slug. 143 | When the resource is returned from the server the next time 144 | it will be slugified. 145 | 146 | Args: 147 | names (Iterable[str]): Iterable with names such as a list or set 148 | """ 149 | for name in names: 150 | self.add_tag(name) 151 | 152 | def add_tag(self, name: str) -> None: 153 | """Add a new tag. 154 | 155 | .. Note: We use the provided name also as the slug. 156 | When the resource is returned from the server the next time 157 | it will be slugified. 158 | 159 | Args: 160 | name (str): Name of the tag 161 | """ 162 | self._data["tags"][name] = name 163 | 164 | def del_tag(self, name: str = None, slug: str = None) -> None: 165 | """Delete a tag. 166 | 167 | We can delete a tag by using the slug or actual name. 168 | Names and slugs are case insensitive. 169 | 170 | Args: 171 | name (str): name to remove 172 | slug (str): slug to remove 173 | 174 | Raises: 175 | KeyError: if tag does not exist 176 | """ 177 | if name: 178 | for _slug, _name in self._data["tags"].items(): 179 | if name.lower() == _name.lower(): 180 | del self._data["tags"][_slug] 181 | break 182 | else: 183 | raise KeyError 184 | elif slug: 185 | del self._data["tags"][slug.lower()] 186 | 187 | def has_tag(self, name: str = None, slug: str = None) -> bool: 188 | """Check if a tag exists by name or slug. 189 | 190 | Tags and slugs are case insensitive. 191 | 192 | Args: 193 | name (str): Name of the tag 194 | slug (str): Slug for the tag 195 | 196 | Returns: 197 | bool: If the tag exists 198 | """ 199 | if slug: 200 | return slug.lower() in self.tags 201 | elif name: 202 | for _name in self.tags.values(): 203 | if name.lower() == _name.lower(): 204 | return True 205 | else: 206 | return False 207 | 208 | return False 209 | 210 | def update(self): 211 | """ 212 | Posts the values in the resource to the server. 213 | 214 | Example:: 215 | 216 | # Change an attribute and save the resource 217 | >> resource.value = something 218 | >> updated_resource = resource.update() 219 | 220 | Returns: 221 | Resource: The updated resource from the server 222 | """ 223 | # Transform tags into an iterable 224 | data = copy.deepcopy(self.attrs) 225 | if data.get("tags") is not None: 226 | data["tags"] = data["tags"].values() 227 | 228 | return self._manager.update(self.get("id"), **data) 229 | 230 | 231 | class ComponentManager(Manager): 232 | resource_class = Component 233 | path = "components" 234 | 235 | def create( 236 | self, 237 | *, 238 | name: str, 239 | status: int, 240 | description: str = None, 241 | link: str = None, 242 | order: int = None, 243 | group_id: int = None, 244 | enabled: bool = True, 245 | tags: Iterable[str] = None 246 | ): 247 | """Create a component. 248 | 249 | Keyword Args: 250 | name (str): Name of the component 251 | status (int): Status if of the component (see enums module) 252 | description (str): Description of the component (required) 253 | link (str): Link to the component 254 | order (int): Order of the component in its group 255 | group_id (int): The group it belongs to 256 | enabled (bool): Enabled status 257 | tags (Iterable[str]): A list, set or other iterable containing string tags 258 | 259 | Returns: 260 | :py:class:`Component` instance 261 | """ 262 | if status not in enums.COMPONENT_STATUS_LIST: 263 | raise ValueError( 264 | "Invalid status id '{}'. Valid values :{}".format( 265 | status, 266 | enums.COMPONENT_STATUS_LIST, 267 | ) 268 | ) 269 | 270 | if tags is not None and not isinstance(tags, abc.Iterable): 271 | raise ValueError("tags is not an iterable") 272 | 273 | if isinstance(tags, str): 274 | raise ValueError( 275 | "tags cannot be a string. It needs to be an iterable of strings." 276 | ) 277 | 278 | return self._create( 279 | self.path, 280 | { 281 | "name": name, 282 | "description": description, 283 | "status": status, 284 | "link": link, 285 | "order": order, 286 | "group_id": group_id, 287 | "enabled": enabled, 288 | "tags": ",".join(tags) if tags else None, 289 | }, 290 | ) 291 | 292 | def update( 293 | self, 294 | component_id: int, 295 | *, 296 | status: int, 297 | name: str = None, 298 | description: str = None, 299 | link: str = None, 300 | order: int = None, 301 | group_id: int = None, 302 | enabled: bool = None, 303 | tags: Iterable[str] = None, 304 | **kwargs 305 | ) -> Component: 306 | """Update a component by id. 307 | 308 | Args: 309 | component_id (int): The component to update 310 | 311 | Keyword Args: 312 | status (int): Status of the component (see enums) 313 | name (str): New name 314 | description (str): New description 315 | link (str): Link to component 316 | order (int): Order in component group 317 | group_id (int): Component group id 318 | enabled (bool): Enable status of component 319 | tags (Iterable[str]): Iterable of tag strings 320 | 321 | Returns: 322 | Updated Component from server 323 | """ 324 | if tags is not None and not isinstance(tags, abc.Iterable): 325 | raise ValueError("tags is not an iterable") 326 | 327 | return self._update( 328 | self.path, 329 | component_id, 330 | self._build_data_dict( 331 | status=status, 332 | name=name, 333 | description=description, 334 | link=link, 335 | order=order, 336 | group_id=group_id, 337 | enabled=enabled, 338 | tags=",".join(tags) if tags else None, 339 | ), 340 | ) 341 | 342 | def list( 343 | self, page: int = 1, per_page: int = 20 344 | ) -> Generator[Component, None, None]: 345 | """List all components 346 | 347 | Keyword Args: 348 | page (int): The page to start listing 349 | per_page (int): Number of entries per page 350 | 351 | Returns: 352 | Generator of Component instances 353 | """ 354 | yield from self._list_paginated(self.path, page=page, per_page=per_page) 355 | 356 | def get(self, component_id: int) -> Component: 357 | """Get a component by id 358 | 359 | Args: 360 | component_id (int): Id of the component 361 | 362 | Returns: 363 | Component instance 364 | 365 | Raises: 366 | HttpError: if not found 367 | """ 368 | return self._get(self.path, component_id) 369 | 370 | def delete(self, component_id: int) -> None: 371 | """Delete a component 372 | 373 | Args: 374 | component_id (int): Id of the component 375 | 376 | Raises: 377 | HTTPError: if component do not exist 378 | """ 379 | self._delete(self.path, component_id) 380 | 381 | def count(self) -> int: 382 | """Count the number of components 383 | 384 | Returns: 385 | int: Total number of components 386 | """ 387 | return self._count(self.path) 388 | -------------------------------------------------------------------------------- /tests/fakeapi.py: -------------------------------------------------------------------------------- 1 | """Fake cachet api""" 2 | import math 3 | import random 4 | import re 5 | import string 6 | from datetime import datetime 7 | 8 | from requests.exceptions import HTTPError 9 | from cachetclient.v1 import enums 10 | 11 | 12 | class FakeData: 13 | 14 | def __init__(self, routes): 15 | self.routes = routes 16 | self.data = [] 17 | self.map = {} 18 | self.last_id = 0 19 | 20 | def add_entry(self, entry): 21 | """Add a data entry""" 22 | self.data.append(entry) 23 | self.map[entry['id']] = entry 24 | 25 | def get_by_id(self, resource_id): 26 | """Get a resource by id""" 27 | resource_id = int(resource_id) 28 | data = self.map.get(resource_id) 29 | if data is None: 30 | raise HTTPError("404") 31 | 32 | return data 33 | 34 | def delete_by_id(self, resource_id): 35 | """Delete a resource""" 36 | resource_id = int(resource_id) 37 | resource = self.map.get(resource_id) 38 | if not resource: 39 | raise HTTPError("404") 40 | 41 | del self.map[resource_id] 42 | self.data.remove(resource) 43 | 44 | def next_id(self): 45 | """Generate unique instance id""" 46 | self.last_id += 1 47 | return self.last_id 48 | 49 | def _get(self, resource_id): 50 | """Get a single resource""" 51 | resource_id = int(resource_id) 52 | data = self.get_by_id(resource_id) 53 | return FakeHttpResponse(data={'data': data}) 54 | 55 | def _list(self, per_page=20, page=1, filter_data=None): 56 | """Generic list with pagination""" 57 | start = per_page * (page - 1) 58 | end = per_page * page 59 | 60 | # Filter data on a single key/value pair 61 | data = self.data 62 | if filter_data: 63 | item = filter_data.popitem() 64 | data = [i for i in self.data if item in i.items()] 65 | 66 | entries = data[start:end] 67 | 68 | return FakeHttpResponse( 69 | data={ 70 | 'meta': { 71 | 'pagination': { 72 | 'total': len(self.data), 73 | 'count': len(entries), 74 | 'per_page': per_page, 75 | 'current_page': page, 76 | 'total_pages': math.ceil(len(self.data) / per_page), 77 | } 78 | }, 79 | 'data': entries, 80 | } 81 | ) 82 | 83 | 84 | class FakeSubscribers(FakeData): 85 | 86 | def get(self, params=None, **kwargs): 87 | """List only supported""" 88 | return super()._list( 89 | per_page=params.get('per_page') or 20, 90 | page=params.get('page') or 1, 91 | ) 92 | 93 | def post(self, params=None, data=None): 94 | instance = { 95 | "id": self.next_id(), 96 | "email": data['email'], 97 | "verify_code": ''.join(random.choice(string.ascii_lowercase) for i in range(16)), 98 | "verified_at": "2015-07-24 14:42:24", 99 | "created_at": "2015-07-24 14:42:24", 100 | "updated_at": "2015-07-24 14:42:24", 101 | "global": True, 102 | } 103 | self.add_entry(instance) 104 | return FakeHttpResponse(data={'data': instance}) 105 | 106 | def delete(self, subscriber_id=None, **kwargs): 107 | self.delete_by_id(subscriber_id) 108 | return FakeHttpResponse() 109 | 110 | 111 | class FakeComponents(FakeData): 112 | 113 | def get(self, component_id=None, params=None, **kwargs): 114 | if component_id is None: 115 | return super()._list( 116 | per_page=params.get('per_page') or 20, 117 | page=params.get('page') or 1, 118 | ) 119 | else: 120 | return super()._get(component_id) 121 | 122 | def post(self, params=None, data=None): 123 | instance = { 124 | "id": self.next_id(), 125 | "name": data.get('name'), 126 | "description": data.get('description'), 127 | "link": data.get('link'), 128 | "status": data.get('status'), 129 | "status_name": "Operational", 130 | "order": data.get('order'), 131 | "group_id": data.get('group_id'), 132 | "created_at": "2015-08-01 12:00:00", 133 | "updated_at": "2015-08-01 12:00:00", 134 | "deleted_at": None, 135 | "tags": self._transform_tags(data.get('tags')) 136 | } 137 | self.add_entry(instance) 138 | return FakeHttpResponse(data={'data': instance}) 139 | 140 | def put(self, component_id=None, params=None, data=None): 141 | # TODO: Rules on what field can be updated 142 | instance = self.get_by_id(component_id) 143 | instance.update(data) 144 | instance['tags'] = self._transform_tags(instance.get('tags')) 145 | return FakeHttpResponse(data={'data': instance}) 146 | 147 | def delete(self, component_id=None, params=None, data=None): 148 | self.delete_by_id(component_id) 149 | return FakeHttpResponse() 150 | 151 | def _transform_tags(self, tags): 152 | return {self._slugify(v): v for v in tags.split(',')} if tags else None 153 | 154 | def _slugify(self, name) -> str: 155 | """Slugify a tag name""" 156 | return re.sub(r'[\W_]+', '-', name.lower()) 157 | 158 | 159 | class FakeComponentGroups(FakeData): 160 | 161 | def get(self, group_id=None, params=None, **kwargs): 162 | if group_id is None: 163 | return super()._list( 164 | per_page=params.get('per_page') or 20, 165 | page=params.get('page') or 1, 166 | ) 167 | else: 168 | return super()._get(group_id) 169 | 170 | def post(self, params=None, data=None): 171 | instance = { 172 | 'id': self.next_id(), 173 | 'name': data.get('name'), 174 | 'order': data.get('order'), 175 | 'visible': data.get('visible'), 176 | 'collapsed': data.get('collapsed'), 177 | 'updated_at': '2015-11-07 16:35:13', 178 | 'created_at': '2015-11-07 16:35:13', 179 | 'enabled_components': [], 180 | } 181 | self.add_entry(instance) 182 | return FakeHttpResponse(data={'data': instance}) 183 | 184 | def put(self, group_id=None, params=None, data=None): 185 | # TODO: Rules on what field can be updated 186 | instance = self.get_by_id(group_id) 187 | instance.update(data) 188 | return FakeHttpResponse(data={'data': instance}) 189 | 190 | def delete(self, group_id=None, params=None, data=None): 191 | self.delete_by_id(group_id) 192 | return FakeHttpResponse() 193 | 194 | 195 | class FakeSchedules(FakeData): 196 | 197 | def get(self, schedule_id=None, params=None, data=None): 198 | if schedule_id: 199 | return self._get(schedule_id) 200 | else: 201 | return self._list( 202 | per_page=params.get('per_page') or 20, 203 | page=params.get('page') or 1, 204 | ) 205 | 206 | def post(self, params=None, data=None): 207 | # cachet takes HH:MM format and converts it to a datetime. Just fake it here. 208 | scheduled_at = data.get('scheduled_at') 209 | if scheduled_at: 210 | scheduled_at += ":00" 211 | completed_at = data.get('completed_at') 212 | if completed_at: 213 | completed_at += ":00" 214 | 215 | instance = { 216 | 'id': self.next_id(), 217 | 'name': data.get('name'), 218 | 'message': data.get('message'), 219 | 'status': data.get('status'), 220 | 'scheduled_at': scheduled_at, 221 | 'completed_at': completed_at, 222 | } 223 | self.add_entry(instance) 224 | return FakeHttpResponse(data={'data': instance}) 225 | 226 | def delete(self, schedule_id=None, params=None, data=None): 227 | self.delete_by_id(schedule_id) 228 | return FakeHttpResponse() 229 | 230 | 231 | class FakeIncidents(FakeData): 232 | 233 | def get(self, incident_id=None, params=None, data=None): 234 | if incident_id: 235 | return self._get(incident_id) 236 | else: 237 | return self._list( 238 | per_page=params.get('per_page') or 20, 239 | page=params.get('page') or 1, 240 | ) 241 | 242 | def post(self, params=None, data=None): 243 | # Fields we don't store but instead triggers behavior 244 | # 'component_status': data.get('component_status'), 245 | # 'template': data.get('template'), 246 | 247 | # Some cachet versions don't handle empty template vars 248 | template_vars = data.get('vars') 249 | if template_vars is None: 250 | raise ValueError("tempate vars is None") 251 | 252 | instance = { 253 | 'id': self.next_id(), 254 | 'name': data.get('name'), 255 | 'message': data.get('message'), 256 | 'status': data.get('status'), 257 | 'human_status': enums.incident_status_human(data.get('status')), 258 | 'visible': data.get('visible'), 259 | 'component_id': data.get('component_id'), 260 | 'notify': data.get('notify'), 261 | 'created_at': '2019-05-25 15:21:34', 262 | 'occurred_at': data.get('occurred_at') or '2019-05-25 15:21:34', 263 | 'scheduled_at': '2019-05-25 15:21:34', 264 | 'updated_at': '2019-05-25 15:21:34', 265 | } 266 | self.add_entry(instance) 267 | return FakeHttpResponse(data={'data': instance}) 268 | 269 | def put(self, incident_id=None, params=None, data=None): 270 | # TODO: Rules on what field can be updated 271 | instance = self.get_by_id(incident_id) 272 | 273 | # Required params 274 | instance['name'] = data['name'] 275 | instance['message'] = data['message'] 276 | instance['status'] = data['status'] 277 | instance['visible'] = data['visible'] 278 | 279 | # Optional only update if value is present 280 | for key, value in data.items(): 281 | if key in instance and value is not None: 282 | instance[key] = value 283 | 284 | return FakeHttpResponse(data={'data': instance}) 285 | 286 | def delete(self, incident_id=None, params=None, data=None): 287 | self.delete_by_id(incident_id) 288 | return FakeHttpResponse() 289 | 290 | 291 | class FakeIncidentUpdates(FakeData): 292 | 293 | def post(self, incident_id=None, params=None, data=None): 294 | new_id = self.next_id() 295 | instance = { 296 | 'id': new_id, 297 | 'incident_id': int(incident_id), 298 | 'status': data['status'], 299 | 'human_status': enums.incident_status_human(data['status']), 300 | 'message': data['message'], 301 | 'user_id': 1, # We assume user 1 always 302 | 'permalink': 'http://status.test/incidents/1#update-{}'.format(new_id), 303 | 'created_at': '2019-05-25 15:21:34', 304 | 'updated_at': '2019-05-25 15:21:34', 305 | } 306 | self.add_entry(instance) 307 | return FakeHttpResponse(data={'data': instance}) 308 | 309 | def put(self, incident_id=None, update_id=None, params=None, data=None): 310 | instance = self.get_by_id(update_id) 311 | instance.update({ 312 | 'status': data['status'], 313 | 'message': data['message'], 314 | }) 315 | return FakeHttpResponse(data={'data': instance}) 316 | 317 | def get(self, incident_id=None, update_id=None, params=None, data=None): 318 | if update_id is None: 319 | return super()._list( 320 | per_page=params.get('per_page') or 20, 321 | page=params.get('page') or 1, 322 | ) 323 | else: 324 | return super()._get(update_id) 325 | 326 | 327 | class FakeMetrics(FakeData): 328 | 329 | def get(self, metric_id=None, params=None, data=None): 330 | if metric_id: 331 | return self._get(metric_id) 332 | else: 333 | return self._list( 334 | per_page=params.get('per_page') or 20, 335 | page=params.get('page') or 1, 336 | ) 337 | 338 | def post(self, params=None, data=None): 339 | 340 | instance = { 341 | 'id': self.next_id(), 342 | 'name': data.get('name'), 343 | 'description': data.get('description'), 344 | 'suffix': data.get('suffix'), 345 | 'default_value': data.get('default_value'), 346 | 'display_chart': data.get('display_chart'), 347 | 'created_at': '2019-05-25 15:21:34', 348 | 'updated_at': '2019-05-25 15:21:34', 349 | } 350 | self.add_entry(instance) 351 | return FakeHttpResponse(data={'data': instance}) 352 | 353 | 354 | def delete(self, metric_id=None, params=None, data=None): 355 | self.delete_by_id(metric_id) 356 | return FakeHttpResponse() 357 | 358 | 359 | class FakeMetricPoints(FakeData): 360 | 361 | def post(self, metric_id=None, params=None, data=None): 362 | new_id = self.next_id() 363 | instance = { 364 | 'id': new_id, 365 | 'metric_id': int(metric_id), 366 | 'value': data['value'], 367 | 'created_at': '2019-05-25 15:21:34', 368 | 'updated_at': '2019-05-25 15:21:34', 369 | } 370 | self.add_entry(instance) 371 | return FakeHttpResponse(data={'data': instance}) 372 | 373 | def get(self, metric_id=None, params=None, data=None): 374 | return self._list( 375 | per_page=params.get('per_page') or 20, 376 | page=params.get('page') or 1, 377 | ) 378 | 379 | def delete(self, metric_id=None, point_id=None, params=None, data=None): 380 | self.delete_by_id(metric_id, point_id) 381 | return FakeHttpResponse() 382 | 383 | class FakePing(FakeData): 384 | 385 | def get(self, *args, **kwargs): 386 | return FakeHttpResponse(data={"data": "Pong!"}) 387 | 388 | 389 | class FakeVersion(FakeData): 390 | 391 | def get(self, *args, **kwargs): 392 | return FakeHttpResponse(data={ 393 | "meta": { 394 | "on_latest": True, 395 | "latest": { 396 | "tag_name": "v2.3.10", 397 | "prelease": False, 398 | "draft": False, 399 | } 400 | }, 401 | "data": "2.3.11-dev", 402 | }) 403 | 404 | 405 | class Routes: 406 | """Requesting routing""" 407 | 408 | def __init__(self): 409 | self.ping = FakePing(self) 410 | self.version = FakeVersion(self) 411 | self.components = FakeComponents(self) 412 | self.component_groups = FakeComponentGroups(self) 413 | self.incidents = FakeIncidents(self) 414 | self.incident_updates = FakeIncidentUpdates(self) 415 | self.metrics = FakeMetrics(self) 416 | self.metric_points = FakeMetricPoints(self) 417 | self.subscribers = FakeSubscribers(self) 418 | self.schedules = FakeSchedules(self) 419 | 420 | self._routes = [ 421 | (r'^ping', self.ping, ['get']), 422 | (r'^version', self.version, ['get']), 423 | (r'^components/groups/(?P\w+)', self.component_groups, ['get', 'put', 'delete']), 424 | (r'^components/groups', self.component_groups, ['get', 'post']), 425 | (r'^components/(?P\w+)', self.components, ['get', 'post', 'put', 'delete']), 426 | (r'^components', self.components, ['get', 'post']), 427 | (r'^incidents/(?P\w+)/updates/(?P\w+)', self.incident_updates, ['get', 'post', 'put', 'delete']), 428 | (r'^incidents/(?P\w+)/updates', self.incident_updates, ['get', 'post']), 429 | (r'^incidents/(?P\w+)', self.incidents, ['get', 'put', 'delete']), 430 | (r'^incidents', self.incidents, ['get', 'post']), 431 | (r'^metrics/(?P\w+)/points/(?P\w+)', self.metric_points, ['delete']), 432 | (r'^metrics/(?P\w+)/points', self.metric_points, ['get', 'post']), 433 | (r'^metrics/(?P\w+)', self.metrics, ['get', 'delete']), 434 | (r'^metrics', self.metrics, ['get', 'post']), 435 | (r'^subscribers/(?P\w+)', self.subscribers, ['delete']), 436 | (r'^subscribers', self.subscribers, ['get', 'post']), 437 | (r'^schedules/(?P\w+)', self.schedules, ['get', 'delete']), 438 | (r'^schedules', self.schedules, ['get', 'post']) 439 | ] 440 | 441 | def dispatch(self, method, path, data=None, params=None): 442 | for route in self._routes: 443 | pattern, manager, allowed_methods = route 444 | # print(pattern, manager, allowed_methods) 445 | 446 | match = re.search(pattern, path) 447 | if not match: 448 | continue 449 | 450 | if method in allowed_methods: 451 | func = getattr(manager, method, None) 452 | if func: 453 | return func(params=params, data=data, **match.groupdict()) 454 | 455 | raise ValueError("Method '{}' not allowed for '{}'".format(method, path)) 456 | 457 | 458 | class FakeHttpClient: 459 | """Fake implementation of the httpclient""" 460 | is_fake_client = True 461 | 462 | def __init__(self, base_url, api_token, timeout=None, verify_tls=True, user_agent=None): 463 | self.routes = Routes() 464 | self.base_url = base_url 465 | self.api_token = api_token 466 | self.timeout = timeout 467 | self.verify_tls = verify_tls 468 | self.user_agent = user_agent 469 | 470 | def get(self, path, params=None): 471 | return self.request('get', path, params=params) 472 | 473 | def post(self, path, data=None, params=None): 474 | return self.request('post', path, data=data, params=params) 475 | 476 | def put(self, path, data=None, params=None): 477 | return self.request('put', path, data=data, params=params) 478 | 479 | def delete(self, path, resource_id): 480 | return self.request('delete', "{}/{}".format(path, resource_id)) 481 | 482 | def request(self, method, path, params=None, data=None): 483 | return self.routes.dispatch(method, path, params=params, data=data) 484 | 485 | 486 | class FakeHttpResponse: 487 | 488 | def __init__(self, data=None, status_code=200): 489 | self.status_code = status_code 490 | self._data = data 491 | 492 | def json(self): 493 | return self._data 494 | 495 | def raise_for_status(self): 496 | if self.status_code > 300: 497 | raise HTTPError(self.status_code) 498 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape 142 | 143 | # Enable the message, report, category or checker with the given id(s). You can 144 | # either give multiple identifier separated by comma (,) or put this option 145 | # multiple time (only on the command line, not in the configuration file where 146 | # it should appear only once). See also the "--disable" option for examples. 147 | enable=c-extension-no-member 148 | 149 | 150 | [REPORTS] 151 | 152 | # Python expression which should return a note less than 10 (10 is the highest 153 | # note). You have access to the variables errors warning, statement which 154 | # respectively contain the number of errors / warnings messages and the total 155 | # number of statements analyzed. This is used by the global evaluation report 156 | # (RP0004). 157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 158 | 159 | # Template used to display messages. This is a python new-style format string 160 | # used to format the message information. See doc for all details. 161 | #msg-template= 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, json 164 | # and msvs (visual studio). You can also give a reporter class, e.g. 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Tells whether to display a full report or only the messages. 169 | reports=no 170 | 171 | # Activate the evaluation score. 172 | score=yes 173 | 174 | 175 | [REFACTORING] 176 | 177 | # Maximum number of nested blocks for function / method body 178 | max-nested-blocks=5 179 | 180 | # Complete name of functions that never returns. When checking for 181 | # inconsistent-return-statements if a never returning function is called then 182 | # it will be considered as an explicit return statement and no message will be 183 | # printed. 184 | never-returning-functions=sys.exit 185 | 186 | 187 | [LOGGING] 188 | 189 | # Format style used to check logging format string. `old` means using % 190 | # formatting, while `new` is for `{}` formatting. 191 | logging-format-style=old 192 | 193 | # Logging modules to check that the string format arguments are in logging 194 | # function parameter format. 195 | logging-modules=logging 196 | 197 | 198 | [SPELLING] 199 | 200 | # Limits count of emitted suggestions for spelling mistakes. 201 | max-spelling-suggestions=4 202 | 203 | # Spelling dictionary name. Available dictionaries: none. To make it working 204 | # install python-enchant package.. 205 | spelling-dict= 206 | 207 | # List of comma separated words that should not be checked. 208 | spelling-ignore-words= 209 | 210 | # A path to a file that contains private dictionary; one word per line. 211 | spelling-private-dict-file= 212 | 213 | # Tells whether to store unknown words to indicated private dictionary in 214 | # --spelling-private-dict-file option instead of raising a message. 215 | spelling-store-unknown-words=no 216 | 217 | 218 | [MISCELLANEOUS] 219 | 220 | # List of note tags to take in consideration, separated by a comma. 221 | notes=FIXME, 222 | XXX, 223 | TODO 224 | 225 | 226 | [TYPECHECK] 227 | 228 | # List of decorators that produce context managers, such as 229 | # contextlib.contextmanager. Add to this list to register other decorators that 230 | # produce valid context managers. 231 | contextmanager-decorators=contextlib.contextmanager 232 | 233 | # List of members which are set dynamically and missed by pylint inference 234 | # system, and so shouldn't trigger E1101 when accessed. Python regular 235 | # expressions are accepted. 236 | generated-members= 237 | 238 | # Tells whether missing members accessed in mixin class should be ignored. A 239 | # mixin class is detected if its name ends with "mixin" (case insensitive). 240 | ignore-mixin-members=yes 241 | 242 | # Tells whether to warn about missing members when the owner of the attribute 243 | # is inferred to be None. 244 | ignore-none=yes 245 | 246 | # This flag controls whether pylint should warn about no-member and similar 247 | # checks whenever an opaque object is returned when inferring. The inference 248 | # can return multiple potential results while evaluating a Python object, but 249 | # some branches might not be evaluated, which results in partial inference. In 250 | # that case, it might be useful to still emit no-member and other checks for 251 | # the rest of the inferred objects. 252 | ignore-on-opaque-inference=yes 253 | 254 | # List of class names for which member attributes should not be checked (useful 255 | # for classes with dynamically set attributes). This supports the use of 256 | # qualified names. 257 | ignored-classes=optparse.Values,thread._local,_thread._local 258 | 259 | # List of module names for which member attributes should not be checked 260 | # (useful for modules/projects where namespaces are manipulated during runtime 261 | # and thus existing member attributes cannot be deduced by static analysis. It 262 | # supports qualified module names, as well as Unix pattern matching. 263 | ignored-modules= 264 | 265 | # Show a hint with possible names when a member name was not found. The aspect 266 | # of finding the hint is based on edit distance. 267 | missing-member-hint=yes 268 | 269 | # The minimum edit distance a name should have in order to be considered a 270 | # similar match for a missing member name. 271 | missing-member-hint-distance=1 272 | 273 | # The total number of similar names that should be taken in consideration when 274 | # showing a hint for a missing member. 275 | missing-member-max-choices=1 276 | 277 | 278 | [VARIABLES] 279 | 280 | # List of additional names supposed to be defined in builtins. Remember that 281 | # you should avoid defining new builtins when possible. 282 | additional-builtins= 283 | 284 | # Tells whether unused global variables should be treated as a violation. 285 | allow-global-unused-variables=yes 286 | 287 | # List of strings which can identify a callback function by name. A callback 288 | # name must start or end with one of those strings. 289 | callbacks=cb_, 290 | _cb 291 | 292 | # A regular expression matching the name of dummy variables (i.e. expected to 293 | # not be used). 294 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 295 | 296 | # Argument names that match this expression will be ignored. Default to name 297 | # with leading underscore. 298 | ignored-argument-names=_.*|^ignored_|^unused_ 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # List of qualified module names which can have objects that can redefine 304 | # builtins. 305 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 306 | 307 | 308 | [FORMAT] 309 | 310 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 311 | expected-line-ending-format= 312 | 313 | # Regexp for a line that is allowed to be longer than the limit. 314 | ignore-long-lines=^\s*(# )??$ 315 | 316 | # Number of spaces of indent required inside a hanging or continued line. 317 | indent-after-paren=4 318 | 319 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 320 | # tab). 321 | indent-string=' ' 322 | 323 | # Maximum number of characters on a single line. 324 | max-line-length=100 325 | 326 | # Maximum number of lines in a module. 327 | max-module-lines=1000 328 | 329 | # List of optional constructs for which whitespace checking is disabled. `dict- 330 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 331 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 332 | # `empty-line` allows space-only lines. 333 | no-space-check=trailing-comma, 334 | dict-separator 335 | 336 | # Allow the body of a class to be on the same line as the declaration if body 337 | # contains single statement. 338 | single-line-class-stmt=no 339 | 340 | # Allow the body of an if to be on the same line as the test if there is no 341 | # else. 342 | single-line-if-stmt=no 343 | 344 | 345 | [SIMILARITIES] 346 | 347 | # Ignore comments when computing similarities. 348 | ignore-comments=yes 349 | 350 | # Ignore docstrings when computing similarities. 351 | ignore-docstrings=yes 352 | 353 | # Ignore imports when computing similarities. 354 | ignore-imports=no 355 | 356 | # Minimum lines number of a similarity. 357 | min-similarity-lines=4 358 | 359 | 360 | [BASIC] 361 | 362 | # Naming style matching correct argument names. 363 | argument-naming-style=snake_case 364 | 365 | # Regular expression matching correct argument names. Overrides argument- 366 | # naming-style. 367 | #argument-rgx= 368 | 369 | # Naming style matching correct attribute names. 370 | attr-naming-style=snake_case 371 | 372 | # Regular expression matching correct attribute names. Overrides attr-naming- 373 | # style. 374 | #attr-rgx= 375 | 376 | # Bad variable names which should always be refused, separated by a comma. 377 | bad-names=foo, 378 | bar, 379 | baz, 380 | toto, 381 | tutu, 382 | tata 383 | 384 | # Naming style matching correct class attribute names. 385 | class-attribute-naming-style=any 386 | 387 | # Regular expression matching correct class attribute names. Overrides class- 388 | # attribute-naming-style. 389 | #class-attribute-rgx= 390 | 391 | # Naming style matching correct class names. 392 | class-naming-style=PascalCase 393 | 394 | # Regular expression matching correct class names. Overrides class-naming- 395 | # style. 396 | #class-rgx= 397 | 398 | # Naming style matching correct constant names. 399 | const-naming-style=UPPER_CASE 400 | 401 | # Regular expression matching correct constant names. Overrides const-naming- 402 | # style. 403 | #const-rgx= 404 | 405 | # Minimum line length for functions/classes that require docstrings, shorter 406 | # ones are exempt. 407 | docstring-min-length=-1 408 | 409 | # Naming style matching correct function names. 410 | function-naming-style=snake_case 411 | 412 | # Regular expression matching correct function names. Overrides function- 413 | # naming-style. 414 | #function-rgx= 415 | 416 | # Good variable names which should always be accepted, separated by a comma. 417 | good-names=i, 418 | j, 419 | k, 420 | ex, 421 | Run, 422 | _ 423 | 424 | # Include a hint for the correct naming format with invalid-name. 425 | include-naming-hint=no 426 | 427 | # Naming style matching correct inline iteration names. 428 | inlinevar-naming-style=any 429 | 430 | # Regular expression matching correct inline iteration names. Overrides 431 | # inlinevar-naming-style. 432 | #inlinevar-rgx= 433 | 434 | # Naming style matching correct method names. 435 | method-naming-style=snake_case 436 | 437 | # Regular expression matching correct method names. Overrides method-naming- 438 | # style. 439 | #method-rgx= 440 | 441 | # Naming style matching correct module names. 442 | module-naming-style=snake_case 443 | 444 | # Regular expression matching correct module names. Overrides module-naming- 445 | # style. 446 | #module-rgx= 447 | 448 | # Colon-delimited sets of names that determine each other's naming style when 449 | # the name regexes allow several styles. 450 | name-group= 451 | 452 | # Regular expression which should only match function or class names that do 453 | # not require a docstring. 454 | no-docstring-rgx=^_ 455 | 456 | # List of decorators that produce properties, such as abc.abstractproperty. Add 457 | # to this list to register other decorators that produce valid properties. 458 | # These decorators are taken in consideration only for invalid-name. 459 | property-classes=abc.abstractproperty 460 | 461 | # Naming style matching correct variable names. 462 | variable-naming-style=snake_case 463 | 464 | # Regular expression matching correct variable names. Overrides variable- 465 | # naming-style. 466 | #variable-rgx= 467 | 468 | 469 | [STRING] 470 | 471 | # This flag controls whether the implicit-str-concat-in-sequence should 472 | # generate a warning on implicit string concatenation in sequences defined over 473 | # several lines. 474 | check-str-concat-over-line-jumps=no 475 | 476 | 477 | [IMPORTS] 478 | 479 | # Allow wildcard imports from modules that define __all__. 480 | allow-wildcard-with-all=no 481 | 482 | # Analyse import fallback blocks. This can be used to support both Python 2 and 483 | # 3 compatible code, which means that the block might have code that exists 484 | # only in one or another interpreter, leading to false positives when analysed. 485 | analyse-fallback-blocks=no 486 | 487 | # Deprecated modules which should not be used, separated by a comma. 488 | deprecated-modules=optparse,tkinter.tix 489 | 490 | # Create a graph of external dependencies in the given file (report RP0402 must 491 | # not be disabled). 492 | ext-import-graph= 493 | 494 | # Create a graph of every (i.e. internal and external) dependencies in the 495 | # given file (report RP0402 must not be disabled). 496 | import-graph= 497 | 498 | # Create a graph of internal dependencies in the given file (report RP0402 must 499 | # not be disabled). 500 | int-import-graph= 501 | 502 | # Force import order to recognize a module as part of the standard 503 | # compatibility libraries. 504 | known-standard-library= 505 | 506 | # Force import order to recognize a module as part of a third party library. 507 | known-third-party=enchant 508 | 509 | 510 | [CLASSES] 511 | 512 | # List of method names used to declare (i.e. assign) instance attributes. 513 | defining-attr-methods=__init__, 514 | __new__, 515 | setUp 516 | 517 | # List of member names, which should be excluded from the protected access 518 | # warning. 519 | exclude-protected=_asdict, 520 | _fields, 521 | _replace, 522 | _source, 523 | _make 524 | 525 | # List of valid names for the first argument in a class method. 526 | valid-classmethod-first-arg=cls 527 | 528 | # List of valid names for the first argument in a metaclass class method. 529 | valid-metaclass-classmethod-first-arg=cls 530 | 531 | 532 | [DESIGN] 533 | 534 | # Maximum number of arguments for function / method. 535 | max-args=5 536 | 537 | # Maximum number of attributes for a class (see R0902). 538 | max-attributes=7 539 | 540 | # Maximum number of boolean expressions in an if statement. 541 | max-bool-expr=5 542 | 543 | # Maximum number of branch for function / method body. 544 | max-branches=12 545 | 546 | # Maximum number of locals for function / method body. 547 | max-locals=15 548 | 549 | # Maximum number of parents for a class (see R0901). 550 | max-parents=7 551 | 552 | # Maximum number of public methods for a class (see R0904). 553 | max-public-methods=20 554 | 555 | # Maximum number of return / yield for function / method body. 556 | max-returns=6 557 | 558 | # Maximum number of statements in function / method body. 559 | max-statements=50 560 | 561 | # Minimum number of public methods for a class (see R0903). 562 | min-public-methods=2 563 | 564 | 565 | [EXCEPTIONS] 566 | 567 | # Exceptions that will emit a warning when being caught. Defaults to 568 | # "BaseException, Exception". 569 | overgeneral-exceptions=BaseException, 570 | Exception 571 | --------------------------------------------------------------------------------