├── requirements ├── doc.txt └── style.txt ├── pypingdom ├── tests │ ├── __init__.py │ ├── resources │ │ └── api │ │ │ └── 2.0 │ │ │ ├── servertime │ │ │ ├── checks │ │ │ └── summary.outage │ │ │ └── 85975 │ ├── test_api.py │ ├── test_check.py │ └── test_client.py ├── __init__.py ├── api.py ├── gui.py ├── maintenance.py ├── check.py └── client.py ├── .travis.yml ├── .pre-commit-config.yaml ├── AUTHORS ├── docs └── source │ ├── index.rst │ └── conf.py ├── MANIFEST.in ├── .gitignore ├── Makefile ├── LICENSE ├── setup.cfg ├── Changelog.rst ├── tox.ini ├── setup.py ├── testing └── manualtest_check.py └── README.rst /requirements/doc.txt: -------------------------------------------------------------------------------- 1 | sphinx==4.3.1 2 | -------------------------------------------------------------------------------- /pypingdom/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for pypingdom.""" 3 | -------------------------------------------------------------------------------- /pypingdom/tests/resources/api/2.0/servertime: -------------------------------------------------------------------------------- 1 | { 2 | "servertime" : 1294237910 3 | } -------------------------------------------------------------------------------- /requirements/style.txt: -------------------------------------------------------------------------------- 1 | pydocstyle>=2.0 2 | flake8_docstrings>=1.1.0 3 | flake8>=3.7.1 4 | flake8-bandit>=1.0.1 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | install: pip install tox-travis 6 | script: tox 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git@github.com:pre-commit/pre-commit-hooks 2 | sha: v0.7.1 3 | hooks: 4 | - id: check-case-conflict 5 | - id: fix-encoding-pragma 6 | - id: check-docstring-first 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ------------------------------------------------------------------------------ 3 | Paolo Sechi (https://github.com/sekipaolo) 4 | Paul Kremer (https://github.com/infothrill) 5 | Kristofer Borgström (https://github.com/kribor) 6 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 2 5 | :caption: Contents: 6 | 7 | 8 | 9 | Indices and tables 10 | ------------------ 11 | 12 | * :ref:`genindex` 13 | * :ref:`modindex` 14 | * :ref:`search` 15 | 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include Makefile 4 | include tox.ini 5 | recursive-include docs *.py 6 | recursive-include docs *.rst 7 | recursive-include pypingdom *.py 8 | recursive-include requirements *.txt 9 | recursive-include testing *.py 10 | recursive-include pypingdom/tests/resources * 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.swp 3 | 4 | # Packages 5 | *.egg 6 | .eggs 7 | .cache 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | develop-eggs 15 | .installed.cfg 16 | .pyc 17 | .idea 18 | _build 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | .DS_Store 28 | private_test_data.yml 29 | .envrc 30 | -------------------------------------------------------------------------------- /pypingdom/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This is the pypingdom package.""" 3 | 4 | from __future__ import absolute_import 5 | from pypingdom.client import Client # noqa: F401 6 | from pypingdom.check import Check # noqa: F401 7 | from pypingdom.maintenance import Maintenance # noqa: F401 8 | 9 | __author__ = 'Paolo Sechi ' 10 | __license__ = "Apache v2.0" 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile 2 | # 3 | 4 | 5 | .PHONY: publish 6 | publish: 7 | @echo "make clean" 8 | @echo "tox -e build" 9 | @echo "tox -e release" 10 | 11 | .PHONY: clean 12 | clean: 13 | @echo "Cleaning up distutils and tox stuff" 14 | rm -rf build dist deb_dist 15 | rm -rf *.egg .eggs *.egg-info 16 | rm -rf .tox 17 | @echo "Cleaning up byte compiled python stuff" 18 | find . -regex "\(.*__pycache__.*\|*.py[co]\)" -delete 19 | 20 | .PHONY: tests 21 | tests: 22 | python testing/manualtest_check.py 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Author: Paolo Sechi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long_description = file: README.rst 3 | 4 | [build_sphinx] 5 | source-dir = docs/source 6 | build-dir = docs/build 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = docs/build/html 11 | 12 | [flake8] 13 | exclude = .git,__pycache__,build,dist,.tox,.eggs,.direnv 14 | max_line_length = 120 15 | 16 | # https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner 17 | [aliases] 18 | test=pytest 19 | 20 | [check-manifest] 21 | # https://pypi.python.org/pypi/check-manifest 22 | ignore = 23 | .pre-commit-config.yaml 24 | .travis.yml 25 | -------------------------------------------------------------------------------- /Changelog.rst: -------------------------------------------------------------------------------- 1 | 0.x.x 2 | ===== 3 | * drop python2 support 4 | * add bearer token support 5 | 6 | 0.2.2 7 | ===== 8 | * pingdom returns `verify_certificate` instead of `encryption` for the check-ssl key 9 | However update api call only accepts `encryption` and will error with `verify_certificate`, 10 | so we replace `verify_certificate` with `encryption` in the returned check. #78 11 | 12 | 0.2.1 13 | ===== 14 | * Sanitize the data passed to PUT /checks/{check_id} by stripping the 15 | 'verify_certificate' parameter for non-http checks. 16 | 17 | 0.2.0 18 | ===== 19 | * added changelog 20 | * switched legacy, unsupported maintenance window getter to supported rest 21 | api method 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = style,py36,py37,docs 3 | skipsdist=True 4 | 5 | [testenv] 6 | commands = 7 | {envpython} setup.py test 8 | 9 | [testenv:style] 10 | deps = -rrequirements/style.txt 11 | commands = flake8 --version 12 | flake8 {posargs} --ignore=D1 13 | 14 | [testenv:docs] 15 | changedir=docs/source 16 | deps = -rrequirements/doc.txt 17 | commands= 18 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 19 | 20 | # Release tooling 21 | [testenv:build] 22 | basepython = python3 23 | skip_install = true 24 | deps = 25 | wheel 26 | setuptools 27 | commands = 28 | python setup.py -q sdist bdist_wheel 29 | 30 | [testenv:release] 31 | basepython = python3 32 | skip_install = true 33 | deps = 34 | {[testenv:build]deps} 35 | twine >= 1.5.0 36 | commands = 37 | {[testenv:build]commands} 38 | twine upload --skip-existing dist/* 39 | 40 | [travis] 41 | python = 42 | 3.6: py36, style, docs 43 | -------------------------------------------------------------------------------- /pypingdom/tests/test_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the api class.""" 3 | 4 | from __future__ import absolute_import 5 | 6 | import os 7 | import unittest 8 | 9 | import requests_mock 10 | 11 | from pypingdom.api import Api 12 | 13 | 14 | def mock_data(path): 15 | """Return json data for mocking.""" 16 | dir_path = os.path.dirname(os.path.realpath(__file__)) 17 | resource_file = os.path.join(dir_path, os.path.normpath('resources'), path.lstrip('/')) 18 | return open(resource_file, mode='r').read() 19 | 20 | 21 | class ApiTestCase(unittest.TestCase): 22 | """Test cases for the api methods.""" 23 | 24 | @requests_mock.Mocker() 25 | def test_request(self, m): 26 | """Test a simple request.""" 27 | api = Api(username='testuser', password='testpass', apikey='mocked') # noqa: S106 28 | m.request('get', api.base_url + 'checks', text=mock_data('/api/2.0/checks')) 29 | res = api.send('get', 'checks', params={'include_tags': True}) 30 | self.assertTrue('checks' in res) 31 | -------------------------------------------------------------------------------- /pypingdom/tests/test_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the Check class.""" 3 | 4 | from __future__ import absolute_import 5 | 6 | import json 7 | import os 8 | import unittest 9 | 10 | from pypingdom import check 11 | 12 | 13 | def mock_data(path): 14 | """Return json data for mocking.""" 15 | dir_path = os.path.dirname(os.path.realpath(__file__)) 16 | resource_file = os.path.join(dir_path, os.path.normpath('resources'), path.lstrip('/')) 17 | return open(resource_file, mode='r').read() 18 | 19 | 20 | class CheckTestCase(unittest.TestCase): 21 | """Test cases for the Check class.""" 22 | 23 | def setUp(self): 24 | unittest.TestCase.setUp(self) 25 | self.checks = json.loads(mock_data('api/2.0/checks'))['checks'] 26 | 27 | def test_check(self): 28 | """Test basic attributes.""" 29 | acheck = check.Check(None, self.checks[0]) 30 | self.assertEqual(acheck.name, self.checks[0]["name"]) 31 | self.assertFalse("fubar" in acheck.tags) 32 | self.assertEqual(len(acheck.tags), len(self.checks[0]["tags"])) 33 | 34 | def test_check_notags(self): 35 | """Test that a check with no tags.""" 36 | self.assertTrue("tags" not in self.checks[2]) # just to be sure ;-) 37 | acheck = check.Check(None, self.checks[2]) 38 | self.assertTrue(isinstance(acheck.tags, list)) 39 | self.assertEqual(len(acheck.tags), 0) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Well this is our setup tools definition for this project.""" 3 | import os 4 | 5 | from setuptools import setup 6 | # from setuptools.config import read_configuration # only works in newer setuptools 7 | # conf = read_configuration("setup.cfg") 8 | # long_description=conf['metadata']['long_description'], 9 | 10 | BASEDIR = os.path.dirname(__file__) 11 | 12 | with open(os.path.join(BASEDIR, 'README.rst'), 'r') as f: 13 | README = f.read() 14 | 15 | 16 | setup(name='pypingdom', 17 | version='0.2.2', 18 | description='Client for Pingdom Services', 19 | long_description=README, 20 | author='Paolo Sechi', 21 | author_email='sekipaolo@gmail.com', 22 | install_requires=['requests>=0.10.8', 'six>=1.10.0', 'packaging==19.2'], 23 | setup_requires=['pytest-runner'], 24 | tests_require=['pytest>=3.0.7', 'requests-mock>=1.3.0'], 25 | url='https://github.com/sekipaolo/pypingdom', 26 | packages=['pypingdom'], 27 | license='Apache v2.0', 28 | platforms='Posix; MacOS X; Windows', 29 | zip_safe=True, 30 | classifiers=['Development Status :: 3 - Alpha', 31 | 'Intended Audience :: Developers', 32 | 'Intended Audience :: System Administrators', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Operating System :: OS Independent', 35 | 'Topic :: System :: Monitoring', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7' 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /pypingdom/tests/resources/api/2.0/checks: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "hostname": "example.com", 5 | "_id": 85975, 6 | "lasterrortime": 1297446423, 7 | "lastresponsetime": 355, 8 | "lasttesttime": 1300977363, 9 | "name": "My check 1", 10 | "resolution": 1, 11 | "status": "up", 12 | "type": "http", 13 | "tags": [ 14 | { 15 | "name": "apache", 16 | "type": "a", 17 | "count": 2 18 | } 19 | ] 20 | }, 21 | { 22 | "hostname": "mydomain.com", 23 | "_id": 161748, 24 | "lasterrortime": 1299194968, 25 | "lastresponsetime": 1141, 26 | "lasttesttime": 1300977268, 27 | "name": "My check 2", 28 | "resolution": 5, 29 | "status": "up", 30 | "type": "ping", 31 | "tags": [ 32 | { 33 | "name": "nginx", 34 | "type": "u", 35 | "count": 1 36 | } 37 | ] 38 | }, 39 | { 40 | "hostname": "example.net", 41 | "_id": 208655, 42 | "lasterrortime": 1300527997, 43 | "lastresponsetime": 800, 44 | "lasttesttime": 1300977337, 45 | "name": "My check 3", 46 | "resolution": 1, 47 | "status": "down", 48 | "type": "http" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /pypingdom/tests/resources/api/2.0/summary.outage/85975: -------------------------------------------------------------------------------- 1 | { 2 | "summary" : { 3 | "states" : [ { 4 | "status" : "up", 5 | "timefrom" : 1293143523, 6 | "timeto" : 1294180263 7 | }, { 8 | "status" : "down", 9 | "timefrom" : 1294180263, 10 | "timeto" : 1294180323 11 | }, { 12 | "status" : "up", 13 | "timefrom" : 1294180323, 14 | "timeto" : 1294223466 15 | }, { 16 | "status" : "down", 17 | "timefrom" : 1294223466, 18 | "timeto" : 1294223523 19 | }, { 20 | "status" : "up", 21 | "timefrom" : 1294223523, 22 | "timeto" : 1294228503 23 | }, { 24 | "status" : "down", 25 | "timefrom" : 1294228503, 26 | "timeto" : 1294228563 27 | }, { 28 | "status" : "up", 29 | "timefrom" : 1294228563, 30 | "timeto" : 1294228623 31 | }, { 32 | "status" : "down", 33 | "timefrom" : 1294228623, 34 | "timeto" : 1294228743 35 | }, { 36 | "status" : "up", 37 | "timefrom" : 1294228743, 38 | "timeto" : 1294228803 39 | }, { 40 | "status" : "down", 41 | "timefrom" : 1294228803, 42 | "timeto" : 1294228987 43 | }, { 44 | "status" : "up", 45 | "timefrom" : 1294228987, 46 | "timeto" : 1294229047 47 | }, { 48 | "status" : "down", 49 | "timefrom" : 1294229047, 50 | "timeto" : 1294229583 51 | }, { 52 | "status" : "up", 53 | "timefrom" : 1294229583, 54 | "timeto" : 1294229643 55 | }, { 56 | "status" : "down", 57 | "timefrom" : 1294229643, 58 | "timeto" : 1294230063 59 | }, { 60 | "status" : "up", 61 | "timefrom" : 1294230063, 62 | "timeto" : 1294389243 63 | } ] 64 | } 65 | } -------------------------------------------------------------------------------- /pypingdom/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | from packaging import version 7 | 8 | 9 | class ApiError(Exception): 10 | 11 | def __init__(self, http_response): 12 | content = http_response.json() 13 | self.status_code = http_response.status_code 14 | self.status_desc = content['error']['statusdesc'] 15 | self.error_message = content['error']['errormessage'] 16 | super(ApiError, self).__init__(self.__str__()) 17 | 18 | def __repr__(self): 19 | return 'pingdom.ApiError: HTTP `%s - %s` returned with message, "%s"' % \ 20 | (self.status_code, self.status_desc, self.error_message) 21 | 22 | def __str__(self): 23 | return self.__repr__() 24 | 25 | 26 | class Api(object): 27 | 28 | def __init__(self, username, password, apikey, email=False, apiversion="2.0"): 29 | self.base_url = "https://api.pingdom.com/api/" + apiversion + "/" 30 | if version.parse(apiversion) < version.parse('3.0'): 31 | self.auth = HTTPBasicAuth(username, password) 32 | self.headers = {'App-Key': apikey} 33 | if email: 34 | self.headers['Account-Email'] = email 35 | else: 36 | self.headers = {'Authorization': 'Bearer ' + apikey} 37 | self.auth = None 38 | 39 | def send(self, method, resource, resource_id=None, data=None, params=None): 40 | if data is None: 41 | data = {} 42 | if params is None: 43 | params = {} 44 | if resource_id is not None: 45 | resource = "%s/%s" % (resource, resource_id) 46 | response = requests.request(method, self.base_url + resource, 47 | auth=self.auth, 48 | headers=self.headers, 49 | data=data, 50 | params=params 51 | ) 52 | if response.status_code != 200: 53 | raise ApiError(response) 54 | else: 55 | return response.json() 56 | -------------------------------------------------------------------------------- /pypingdom/gui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | import json 4 | 5 | import requests 6 | 7 | 8 | class PingdomGuiException(Exception): 9 | 10 | def __init__(self, http_response): 11 | content = json.loads(http_response.content) 12 | self.status_code = http_response.status_code 13 | self.status_desc = content['error']['statusdesc'] 14 | self.error_message = content['error']['errormessage'] 15 | super(PingdomGuiException, self).__init__(self.__str__()) 16 | 17 | def __repr__(self): 18 | return 'pingdom.PingdomGuiException: HTTP `%s - %s` returned with message, "%s"' % \ 19 | (self.status_code, self.status_desc, self.error_message) 20 | 21 | def __str__(self): 22 | return self.__repr__() 23 | 24 | 25 | class Gui(): 26 | 27 | def __init__(self, username, password): 28 | self.__username = username 29 | self.__password = password 30 | self.session = requests.session() 31 | _ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 " \ 32 | + "(KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" 33 | self.headers = { 34 | "accept": "*/*", 35 | "accept-encoding": "gzip, deflate, br", 36 | "accept-language": "en-US,en;q=0.8", 37 | "cache-control": "no-cache", 38 | "content-type": "application/x-www-form-urlencoded; charset=UTF-8", 39 | "origin": "https://my.pingdom.com", 40 | "pragma": "no-cache", 41 | "referer": "https://my.pingdom.com/", 42 | "user-agent": _ua, 43 | "x-requested-with": "XMLHttpRequest" 44 | } 45 | 46 | def send(self, method, url, data=None, params=None): 47 | if data is None: 48 | data = {} 49 | if params is None: 50 | params = {} 51 | response = self.session.request(method, url, data=data, params=params, headers=self.headers) 52 | if response.status_code != 200: 53 | raise PingdomGuiException(response) 54 | return response 55 | 56 | def login(self): 57 | data = {"email": self.__username, "password": self.__password} 58 | self.send('post', 'https://my.pingdom.com/', data) 59 | -------------------------------------------------------------------------------- /pypingdom/maintenance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | import datetime 4 | import time 5 | 6 | 7 | class Maintenance(object): 8 | 9 | def __init__(self, client, json=False, obj=False): 10 | self.client = client 11 | self._id = False 12 | if json: 13 | self.from_json(json) 14 | elif obj: 15 | self.from_obj(obj) 16 | else: 17 | raise Exception("Missing definition: use json or obj parameter") 18 | 19 | def __repr__(self): 20 | checks = [] 21 | for check in self.checks: 22 | if check: 23 | checks.append(check.name) 24 | else: 25 | checks.append("") 26 | return """ 27 | pingdom.Maintenance <{0}> 28 | name: {1} 29 | start: {2} 30 | end: {3} 31 | checks: {4} 32 | """.format(self._id, 33 | self.name, 34 | self.start, 35 | self.stop, 36 | ", ".join(checks)) 37 | 38 | def to_json(self): 39 | check_ids = [str(check._id) for check in self.checks if check] 40 | data = { 41 | # "__csrf_magic": "", 42 | # "id": "", 43 | "description": self.name, 44 | "from-date": "{0}.{1}.{2}.".format(self.start.year, self.start.month, self.start.day), 45 | "from-time": "{0}:{1}".format(self.start.hour, self.start.minute), 46 | "to-date": "{0}.{1}.{2}.".format(self.stop.year, self.stop.month, self.stop.day), 47 | "to-time": "{0}:{1}".format(self.stop.hour, self.stop.minute), 48 | "start": int(time.mktime(self.start.timetuple())), 49 | "end": int(time.mktime(self.stop.timetuple())), 50 | "checks": "[{0}]".format(",".join(check_ids)) 51 | } 52 | return data 53 | 54 | def from_json(self, obj): 55 | self._id = int(obj['id']) 56 | self.name = obj["description"] 57 | self.start = datetime.datetime.fromtimestamp(obj['from']) 58 | self.stop = datetime.datetime.fromtimestamp(obj['to']) 59 | self.checks = [self.client.get_check(_id=int(x)) for x in obj['checks']['uptime']] 60 | 61 | def from_obj(self, obj): 62 | self.name = obj["name"] 63 | self.start = obj['start'] 64 | self.stop = obj['stop'] 65 | self.checks = obj['checks'] 66 | -------------------------------------------------------------------------------- /pypingdom/check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | 5 | class Check(object): 6 | 7 | SKIP_ON_PRINT = ["cached_definition", "_id", "api"] 8 | SKIP_ON_JSON = [ 9 | "api", "alert_policy_name", "cached_definition", "created", "lastresponsetime", 10 | "lasttesttime", "lasterrortime", "_id", "id", "status", "maintenanceids" 11 | ] 12 | 13 | def __init__(self, api, json=False, obj=False): 14 | self.api = api 15 | if json: 16 | self.from_json(json) 17 | elif obj: 18 | self.from_obj(obj) 19 | else: 20 | raise Exception("Missing check definition: use json or obj parameter") 21 | 22 | def __repr__(self): 23 | attr = ["{0}: {1} ".format(k, v) for k, v in self.__dict__.items() if k not in self.SKIP_ON_PRINT] 24 | return "\npingdom.Check <%s> \n %s" % (self._id, "\n ".join(attr)) 25 | 26 | def __contains__(self, x): 27 | return x in self.__dict__ 28 | 29 | def to_json(self): 30 | obj = {} 31 | for k, v in self.__dict__.items(): 32 | 33 | if k in self.SKIP_ON_JSON: 34 | pass 35 | elif k == "tags" and len(v): 36 | obj["tags"] = ",".join(map(lambda x: x['name'], v)) 37 | elif k == "requestheaders": 38 | i = 0 39 | for x, y in v.items(): 40 | obj["requestheader" + str(i)] = x + ":" + y 41 | i = i + 1 42 | else: 43 | obj[k] = v 44 | return obj 45 | 46 | def from_json(self, obj): 47 | for k, v in obj.items(): 48 | if k == "tags": 49 | self.tags = [x["name"] for x in v] 50 | if k == "name": 51 | self.name = v 52 | elif k == "hostname": 53 | self.host = v 54 | elif k == "id": 55 | self._id = v 56 | elif k == "status": 57 | self.paused = bool(v == "paused") 58 | self.status = v 59 | elif k == "type" and isinstance(k, dict): 60 | self.type = list(v.keys())[0] 61 | for x, y in v[self.type].items(): 62 | setattr(self, x, y) 63 | else: 64 | setattr(self, k, v) 65 | if 'tags' not in self: 66 | self.tags = [] 67 | 68 | def from_obj(self, obj): 69 | for k, v in obj.items(): 70 | setattr(self, k, v) 71 | if 'tags' not in self: 72 | self.tags = [] 73 | 74 | def fetch(self): 75 | res = self.api.send('get', "checks", self._id)['check'] 76 | self.from_json(res) 77 | -------------------------------------------------------------------------------- /pypingdom/tests/test_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the client class.""" 3 | 4 | from __future__ import absolute_import 5 | 6 | import os 7 | import unittest 8 | 9 | import requests_mock 10 | 11 | from pypingdom import Client 12 | from pypingdom.check import Check 13 | 14 | 15 | def mock_data(path): 16 | """Return json data for mocking.""" 17 | dir_path = os.path.dirname(os.path.realpath(__file__)) 18 | resource_file = os.path.join(dir_path, os.path.normpath('resources'), path.lstrip('/')) 19 | return open(resource_file, mode='r').read() 20 | 21 | 22 | class ClientTestCase(unittest.TestCase): 23 | """Test cases for the client methods.""" 24 | 25 | @requests_mock.Mocker() 26 | def test_request(self, m): 27 | """Test a simple request.""" 28 | base_url = 'https://api.pingdom.com' 29 | fakepath = '/api/2.0/checks' 30 | m.request('get', base_url + fakepath, text=mock_data(fakepath)) 31 | fakepath = '/api/2.0/servertime' 32 | m.request('get', base_url + fakepath, text=mock_data(fakepath)) 33 | fakepath = '/api/2.0/summary.outage/85975' 34 | m.request('get', base_url + fakepath, text=mock_data(fakepath)) 35 | 36 | client = Client(username='username', # noqa: S106 37 | password='password', 38 | apikey='apikey', 39 | email='email') 40 | 41 | res = client.get_checks() 42 | self.assertEqual(len(res), 3) 43 | self.assertTrue(all(isinstance(x, Check) for x in res)) 44 | check1 = client.get_check(name='My check 1') 45 | self.assertNotEqual(check1, None) 46 | check2 = client.get_check(name='My check 2') 47 | self.assertNotEqual(check1, check2) 48 | 49 | res = client.servertime() 50 | self.assertEqual(res, 1294237910) 51 | 52 | res = client.get_summary_outage('85975') 53 | self.assertTrue('summary' in res) 54 | self.assertTrue('states' in res['summary']) 55 | 56 | @requests_mock.Mocker() 57 | def test_get_checks(self, m): 58 | """Test a simple request.""" 59 | base_url = 'https://api.pingdom.com' 60 | fakepath = '/api/2.0/checks' 61 | m.request('get', base_url + fakepath, text=mock_data(fakepath)) 62 | 63 | client = Client(username='username', # noqa: S106 64 | password='password', 65 | apikey='apikey', 66 | email='email') 67 | 68 | res = client.get_checks() 69 | self.assertEqual(len(res), 3) 70 | self.assertTrue(all(isinstance(x, Check) for x in res)) 71 | 72 | res = client.get_checks(filters={'tags': ['apache']}) 73 | self.assertEqual(len(res), 1) 74 | -------------------------------------------------------------------------------- /testing/manualtest_check.py: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/env python 2 | # -*- coding: utf-8 -*- 3 | import yaml 4 | import datetime 5 | 6 | from six.moves import input 7 | 8 | import pypingdom 9 | 10 | 11 | with open('private_test_data.yml', 'r') as stream: 12 | auth = yaml.safe_load(stream) 13 | 14 | client = pypingdom.Client(username=auth['username'], 15 | password=auth['password'], 16 | apikey=auth['apikey'], 17 | email=auth['email']) 18 | 19 | check_definition = { 20 | 'name': 'Ansible integration test', 21 | 'paused': True, 22 | # 'alert_policy': '', 23 | 'type': 'http', 24 | 'host': 'www.google.com', 25 | 'url': '/', 26 | 'requestheaders': { 27 | 'XCustom': 'my header value' 28 | }, 29 | 'tags': ['pypingdom-test', 'custom-tag'], 30 | 'encryption': False 31 | } 32 | 33 | 34 | def clean(): 35 | global client, check_definition 36 | checks = client.get_checks(filters={'tags': ['pypingdom-test']}) 37 | for check in checks: 38 | print('deleting check %s' % check.name) # noqa: T001 39 | client.delete_check(check) 40 | checks = client.get_checks(filters={'tags': ['pypingdom-test']}) 41 | print(checks) # noqa: T001 42 | assert checks == [] # noqa: S101 43 | 44 | 45 | def create_check(): 46 | global client, check_definition 47 | c = client.create_check(check_definition) 48 | print('created %s' % c.name) # noqa: T001 49 | check = client.get_check(check_definition['name']) 50 | assert check is not None # noqa: S101 51 | 52 | 53 | def update_check(): 54 | global client, check_definition 55 | check = client.get_check(check_definition['name']) 56 | print('updating check %s' % check.name) # noqa: T001 57 | newcheck = client.update_check(check, {'requestheaders': {'XCustom': 'new header value'}}) 58 | assert newcheck.requestheaders['XCustom'] == 'new header value' # noqa: S101 59 | 60 | 61 | def delete_check(): 62 | global client, check_definition 63 | check = client.get_check(check_definition['name']) 64 | print('deleting check %s' % check.name) # noqa: T001 65 | client.delete_check(check) 66 | check = client.get_check(check_definition['name']) 67 | assert check is None # noqa: S101 68 | 69 | 70 | def create_maintenance(): 71 | global client, check_definition 72 | check = client.get_check(check_definition['name']) 73 | start = datetime.datetime.now() + datetime.timedelta(days=2) 74 | stop = start + datetime.timedelta(minutes=20) 75 | print('creating maintenance for check %s' % check.name) 76 | window = client.create_maintenance({ 77 | 'checks': [check], 78 | 'name': 'pypyngdom test maintenance', 79 | 'start': start, 80 | 'stop': stop 81 | }) 82 | return window 83 | 84 | 85 | def delete_maintenance(mid): 86 | check = client.get_check(check_definition['name']) 87 | for m in client.get_maintenances(filters={'checks': [check]}): 88 | print('deleting maintenance %s' % m.name) # noqa: T001 89 | client.delete_maintenance(m) 90 | 91 | 92 | create_check() 93 | input('Continue?') 94 | update_check() 95 | input('Continue?') 96 | create_maintenance() 97 | input('Continue?') 98 | delete_maintenance() 99 | input('Continue?') 100 | delete_check() 101 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pypingdom documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Mar 18 10:48:53 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | sys.path.insert(0, os.path.abspath('../../pypingdom')) 22 | print(sys.path[0]) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.githubpages'] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix(es) of source filenames. 42 | # You can specify multiple suffix as a list of string: 43 | # 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'pypingdom' 52 | copyright = u'2017, Paolo Sechi ' 53 | author = u'Paolo Sechi ' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = u'0.1' 61 | # The full version, including alpha/beta/rc tags. 62 | release = u'0.1.1' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This patterns also effect to html_static_path and html_extra_path 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = True 81 | 82 | 83 | # -- Options for HTML output ---------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'alabaster' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = [] 100 | 101 | 102 | # -- Options for HTMLHelp output ------------------------------------------ 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'pypingdomdoc' 106 | 107 | 108 | # -- Options for LaTeX output --------------------------------------------- 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | 119 | # Additional stuff for the LaTeX preamble. 120 | # 121 | # 'preamble': '', 122 | 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, 130 | # author, documentclass [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, 'pypingdom.tex', u'pypingdom Documentation', 133 | u'Paolo Sechi \\textless{}sekipaolo@gmail.com\\textgreater{}', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output --------------------------------------- 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'pypingdom', u'pypingdom Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'pypingdom', u'pypingdom Documentation', 154 | author, 'pypingdom', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] 157 | 158 | suppress_warnings = ['image.nonlocal_uri'] 159 | -------------------------------------------------------------------------------- /pypingdom/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from .api import Api 5 | from .gui import Gui 6 | from .check import Check 7 | from .maintenance import Maintenance 8 | 9 | 10 | class Client(object): 11 | """Interact with API and GUI.""" 12 | 13 | def __init__(self, username, password, apikey, email, api_version='2.0'): 14 | """ 15 | Initializer. 16 | 17 | :param username: account main email 18 | :param password: account password 19 | :param apikey: Pingdom api key 20 | :param email: required for `Multi-User Authentication 21 | `_. 22 | """ 23 | self.api = Api(username, password, apikey, email, api_version) 24 | self.gui = Gui(username, password) 25 | # cache checks 26 | self.checks = {} 27 | for item in self.api.send('get', "checks", params={"include_tags": True})['checks']: 28 | self.checks[item["name"]] = Check(self.api, json=item) 29 | 30 | def get_check(self, name=None, _id=None): 31 | if name: 32 | return self.checks.get(name, None) 33 | elif _id: 34 | for _name, check in self.checks.items(): 35 | if check._id == _id: 36 | return check 37 | else: 38 | raise Exception("Missing name or _id") 39 | 40 | def get_checks(self, filters=None): 41 | if filters is None: 42 | return [c for c in self.checks.values()] 43 | 44 | return [c for c in self.checks.values() if len(set(u + filters.get("status", c.status) 45 | for u in filters.get("tags", [])).intersection(set([x['name'] + c.status for x in c.tags])))] 46 | 47 | def create_check(self, obj): 48 | c = Check(self.api, obj=obj) 49 | data = c.to_json() 50 | response = self.api.send(method='post', resource='checks', data=data) 51 | c._id = int(response["check"]["id"]) 52 | c.from_json(self.api.send('get', "checks", response["check"]["id"])['check']) 53 | self.checks[c.name] = c 54 | return c 55 | 56 | def delete_check(self, check): 57 | if not check._id: 58 | raise Exception("CheckNotFound %s" % check.name) 59 | self.api.send(method='delete', resource='checks', resource_id=check._id) 60 | self.checks.pop(check.name, None) 61 | 62 | def update_check(self, check, changes): 63 | # ensure definition is updated 64 | check.fetch() 65 | # cache current definition to detect idempotence when modify is called 66 | cached_definition = check.to_json() 67 | check.from_obj(changes) 68 | data = check.to_json() 69 | if data == cached_definition: 70 | return False 71 | # GET /checks (get_checks) returns 'verify_certificate' regardless 72 | if check.type == 'http': 73 | # The http-type API will only accept a parameter called 'encryption' though. 74 | data['encryption'] = changes['encryption'] if 'encryption' in changes else data['verify_certificate'] 75 | del data['verify_certificate'] # 'verify_certificate' is not a valid parameter 76 | del data["type"] # type can't be changed 77 | self.api.send(method='put', resource='checks', resource_id=check._id, data=data) 78 | check.from_json(self.api.send('get', "checks", check._id)['check']) 79 | return check 80 | 81 | def get_maintenances(self, filters=None): 82 | """Return a list of mainenance windows.""" 83 | if filters is None: 84 | filters = {} 85 | res = [] 86 | for mw in (Maintenance(self, json=x) for x in self.api.send('get', 'maintenance')['maintenance']): 87 | if "checks" in filters: 88 | wanted_ids = [check._id for check in filters['checks']] 89 | got_ids = [x._id for x in mw.checks] 90 | if len(set(wanted_ids).intersection(set(got_ids))) == 0: 91 | continue 92 | if "names" in filters and mw.description not in filters['names']: 93 | continue 94 | if "after" in filters and filters["after"] >= mw.start: 95 | continue 96 | if "before" in filters and filters["before"] <= mw.stop: 97 | continue 98 | res.append(mw) 99 | return res 100 | 101 | def create_maintenance(self, obj): 102 | window = Maintenance(self, obj=obj) 103 | self.gui.login() 104 | response = self.gui.send("post", "https://my.pingdom.com/ims/data/maintenance", window.to_json()) 105 | window._id = response.json()["checks_maintenance"]["id"] 106 | return window 107 | 108 | def delete_maintenance(self, window): 109 | self.gui.login() 110 | self.gui.send("delete", "https://my.pingdom.com/ims/data/maintenance", params={"id": window._id}) 111 | 112 | def servertime(self): 113 | return self.api.send(method='get', resource='servertime')['servertime'] 114 | 115 | def get_summary_average(self, checkid, start=None, end=None, probes=None, include_uptime=None, by_country=None, 116 | by_probe=None): 117 | params = {} 118 | if start is not None: 119 | params['from'] = start 120 | if end is not None: 121 | params['to'] = end 122 | if probes is not None: 123 | params['probes'] = probes 124 | if include_uptime is not None: 125 | params['includeuptime'] = include_uptime 126 | if by_country is not None: 127 | params['bycountry'] = by_country 128 | if by_probe is not None: 129 | params['byprobe'] = by_probe 130 | return self.api.send('get', resource="summary.average", resource_id=checkid, params=params) 131 | 132 | def get_summary_outage(self, checkid, start=None, end=None, order="asc"): 133 | params = {} 134 | if start is not None: 135 | params['from'] = start 136 | if end is not None: 137 | params['to'] = end 138 | if order is not None: 139 | params['order'] = order 140 | return self.api.send('get', resource="summary.outage", resource_id=checkid, params=params) 141 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pypingdom 2 | ========= 3 | 4 | .. image:: https://img.shields.io/pypi/v/pypingdom.svg 5 | :target: https://pypi.python.org/pypi/pypingdom 6 | 7 | .. image:: https://img.shields.io/pypi/l/pypingdom.svg 8 | :target: https://pypi.python.org/pypi/pypingdom 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/pypingdom.svg 11 | :target: https://pypi.python.org/pypi/pypingdom 12 | 13 | .. image:: https://travis-ci.org/sekipaolo/pypingdom.svg?branch=master 14 | :target: https://travis-ci.org/sekipaolo/pypingdom 15 | 16 | Python library for interacting with Pingdom services (REST API and maintenance windows). 17 | 18 | 19 | Features 20 | -------- 21 | 22 | 23 | * Support for `Multi-User Authentication `_ 24 | * Check management: create, delete, update, list 25 | * Maintenance windows: create, delete, list 26 | * Fetching outage summaries 27 | 28 | .. warning:: 29 | 30 | Since the Pingdom REST API don't support maintenance windows, we interact 31 | with the Website for it. Therefore this feature is highly fragile and can 32 | *break* at any moment due to frontend changes on the Pingdom website. 33 | 34 | 35 | Requirements 36 | ------------ 37 | 38 | 39 | * Pingdom account 40 | * requests (0.10.8 or newer) 41 | 42 | 43 | Installation 44 | ------------ 45 | 46 | .. code-block:: python 47 | 48 | pip install pypingdom 49 | 50 | 51 | Usage 52 | ----- 53 | 54 | The `client` object will allow you to interact both with the REST API and the 55 | GUI (for maintenance windows). 56 | 57 | .. code-block:: python 58 | 59 | >>> import pypingdom 60 | >>> client = pypingdom.Client(username="username@example.com", 61 | password="your_password", 62 | apikey="your_api_key", 63 | email="your_email") 64 | 65 | the `email` parameter is required for `Multiuser Authentication `_. 66 | 67 | Checks 68 | ------ 69 | 70 | 71 | Since Pingdom does not treat the check name as identifier (as we probably want 72 | to do) the client object will retrieve the check list from the API and cache it 73 | as a dictionary ( check_name => check_instance). You can access it through the 74 | `checks` attribute: 75 | 76 | .. code-block:: python 77 | 78 | >>> client.checks["my awesome check"] 79 | pingdom.Check <1895866> 80 | autoresolve: 0 81 | alert_policy: 2118909 82 | name: example_com 83 | created: 1448565930 84 | lasterrortime: 1489325292 85 | resolution: 1 86 | lastresponsetime: 558 87 | lasttesttime: 1489847772 88 | alert_policy_name: Production Systems 89 | paused: False 90 | host: hostname.example.com 91 | acktimeout: 0 92 | ipv6: False 93 | use_legacy_notifications: False 94 | type: http 95 | tags: [] 96 | 97 | a better way to retrieve a check would be: 98 | 99 | .. code-block:: python 100 | 101 | >>> client.get_check("my awesome check") 102 | 103 | that will return None if the check doesn't exists 104 | 105 | List checks with `production` and `frontend` tags: 106 | 107 | .. code-block:: python 108 | 109 | >>> client.get_checks(filters={"tags": ["production", "frontend"]}) 110 | 111 | Create a check: 112 | 113 | .. code-block:: python 114 | 115 | >>> check_definition = { 116 | "name": "My awesome check", 117 | "paused": True, 118 | "alert_policy": 201745, 119 | "type": "http", 120 | "host": "www.google.com", 121 | "url": "/", 122 | "requestheaders": { 123 | 'XCustom': 'my header value' 124 | }, 125 | "tags": [{"name": "pypingdom-test"}, {"name": "custom-tag"}], 126 | "encryption": False 127 | } 128 | >>> client.create_check(check_definition) 129 | 130 | 131 | Refers to `this page `_ for the list of options. 132 | 133 | When you create or modify a check some related entity need to be referenced by id: 134 | 135 | *Integrations* 136 | 137 | To enable/disable an integration plugins (like webhooks) use the field `integrationids` (array with integer ids to set or "null" tring to remove it) 138 | 139 | *Alert policies* 140 | 141 | To bind an alerting policy use the field `alert_policy` (numeric id to set it or string "null" to disable alerts) 142 | 143 | 144 | Update a check: 145 | 146 | .. code-block:: python 147 | 148 | >>> client.update_check(check, {"paused": True}) 149 | 150 | this will return True if an effective change was sent to the API and False 151 | otherwise (useful for idempotent usage, like ansible modules) 152 | 153 | Delete a check: 154 | 155 | .. code-block:: python 156 | 157 | >>> client.delete_check(check) 158 | 159 | 160 | Maintenance windows 161 | ------------------- 162 | 163 | Retreive maintenance windows for production websites in the last 7 days: 164 | 165 | .. code-block:: python 166 | 167 | >>> import datetime 168 | >>> checks = client.get_checks(filters={"tags": ["production", "frontend"]}) 169 | >>> start = datetime.datetime.now() - datetime.timedelta(days=7) 170 | >>> client.get_maintenances(filters={"checks": checks, "after": start}) 171 | 172 | Create a 1 hour maintenance window for production websites: 173 | 174 | .. code-block:: python 175 | 176 | >>> start = datetime.datetime.now() + datetime.timedelta(minutes=10) 177 | >>> end = start + datetime.timedelta(hours=1) 178 | 179 | >>> window = client.create_maintenance({"checks": checks, "name": "pypingdom test maintenance", "start": start, "stop": end}) 180 | 181 | Delete future maintenance windows: 182 | 183 | .. code-block:: python 184 | 185 | >>> windows = client.get_maintenances(filters={"checks": checks, "after": datetime.datetime.now()}): 186 | >>> for m in maintenances: 187 | client.delete_maintenance(m) 188 | 189 | 190 | Reporting/summary 191 | ------------------- 192 | 193 | Retrieve average response time and uptime summaries: 194 | 195 | .. code-block:: python 196 | 197 | >>> checkid = client.get_check("my awesome check")._id 198 | >>> start = int(time.time()) - 30*24*60*60 # 30 days back 199 | >>> end = time.time() 200 | >>> client.get_summary_average(checkid, start, end, include_uptime="true") --------------------------------------------------------------------------------