├── tests
├── __init__.py
├── conftest.py
├── requirements.txt
├── test_client.py
├── test_gauge.py
├── test_counter.py
├── test_connection.py
└── test_timer.py
├── docs
├── requirements.txt
├── statsd.raw.rst
├── statsd.gauge.rst
├── statsd.timer.rst
├── statsd.client.rst
├── statsd.counter.rst
├── statsd.connection.rst
├── _theme
│ ├── wolph
│ │ ├── theme.conf
│ │ ├── relations.html
│ │ ├── layout.html
│ │ └── static
│ │ │ ├── small_flask.css
│ │ │ └── flasky.css_t
│ ├── LICENSE
│ └── flask_theme_support.py
├── index.rst
├── Makefile
├── make.bat
└── conf.py
├── .gitignore
├── coverage.rc
├── statsd
├── __about__.py
├── compat.py
├── average.py
├── __init__.py
├── raw.py
├── connection.py
├── counter.py
├── gauge.py
├── client.py
└── timer.py
├── .coveragerc
├── .travis.yml
├── setup.cfg
├── tox.ini
├── setup.py
├── LICENSE
└── README.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | -e.[tests]
2 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | -e.[docs,tests]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build
3 | dist
4 | *.egg-info
5 | *.egg
6 | .*
7 | cover
8 | docs/_build
9 |
--------------------------------------------------------------------------------
/coverage.rc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = statsd,tests
3 | omit =
4 | */nose/*
5 | */compat.py
6 |
--------------------------------------------------------------------------------
/docs/statsd.raw.rst:
--------------------------------------------------------------------------------
1 | statsd.raw
2 | ============
3 |
4 | .. automodule:: statsd.raw
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/statsd.gauge.rst:
--------------------------------------------------------------------------------
1 | statsd.gauge
2 | ============
3 |
4 | .. automodule:: statsd.gauge
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/statsd.timer.rst:
--------------------------------------------------------------------------------
1 | statsd.timer
2 | ============
3 |
4 | .. automodule:: statsd.timer
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/statsd.client.rst:
--------------------------------------------------------------------------------
1 | statsd.client
2 | =============
3 |
4 | .. automodule:: statsd.client
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/statsd.counter.rst:
--------------------------------------------------------------------------------
1 | statsd.counter
2 | ==============
3 |
4 | .. automodule:: statsd.counter
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/statsd.connection.rst:
--------------------------------------------------------------------------------
1 | statsd.connection
2 | =================
3 |
4 | .. automodule:: statsd.connection
5 | :members:
6 |
7 |
--------------------------------------------------------------------------------
/docs/_theme/wolph/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = flasky.css
4 | pygments_style = flask_theme_support.FlaskyStyle
5 |
6 | [options]
7 | touch_icon =
8 |
--------------------------------------------------------------------------------
/statsd/__about__.py:
--------------------------------------------------------------------------------
1 | __package_name__ = 'python-statsd'
2 | __version__ = '2.1.0'
3 | __author__ = 'Rick van Hattem'
4 | __author_email__ = 'Wolph@wol.ph'
5 | __description__ = (
6 | '''statsd is a client for Etsy's node-js statsd server. '''
7 | '''A proxy for the Graphite stats collection and graphing server.''')
8 | __url__ = 'https://github.com/WoLpH/python-statsd'
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | fail_under = 100
3 | exclude_lines =
4 | pragma: no cover
5 | def __repr__
6 | if self.debug:
7 | if settings.DEBUG
8 | raise AssertionError
9 | raise NotImplementedError
10 | if 0:
11 | if __name__ == .__main__.:
12 |
13 | [run]
14 | branch = True
15 | source =
16 | statsd
17 | tests
18 |
19 | omit =
20 | */mock/*
21 | */nose/*
22 |
23 | [paths]
24 | source =
25 | statsd
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | python:
4 | - "2.7"
5 | - "3.4"
6 | - "3.5"
7 | - "3.6"
8 | - "pypy"
9 |
10 | # command to install dependencies
11 | install:
12 | - pip install .
13 | - pip install coveralls flake8
14 |
15 | # command to run tests
16 | script:
17 | - python setup.py nosetests --cover-erase --with-coverage
18 | - nosetests --with-coverage --cover-min-percentage=100
19 |
20 | before_script: flake8 --ignore=W391 statsd tests
21 |
22 | after_success:
23 | - coveralls
24 |
25 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.rst
3 |
4 | [nosetests]
5 | verbosity=3
6 | with-doctest=1
7 | detailed-errors=1
8 | debug=nose.loader
9 | #pdb=1
10 | #pdb-failures=1
11 | with-coverage=1
12 | cover-package=statsd
13 |
14 | [build_sphinx]
15 | source-dir = docs/
16 | build-dir = docs/_build
17 | all_files = 1
18 |
19 | [upload_sphinx]
20 | upload-dir = docs/_build/html
21 |
22 | [bdist_wheel]
23 | universal = 1
24 |
25 | [flake8]
26 | ignore = W391
27 | exclude = docs/*,statsd/compat.py
28 |
29 | [upload]
30 | sign = 1
31 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27, py33, py34, py35, py36, pypy, flake8, docs
3 | skip_missing_interpreters = True
4 |
5 | [testenv]
6 | deps =
7 | nose
8 | coverage
9 | mock
10 |
11 | commands =
12 | python setup.py nosetests --cover-erase --with-coverage
13 | nosetests --with-coverage --cover-min-percentage=100
14 |
15 | [testenv:flake8]
16 | basepython = python2.7
17 | deps = flake8
18 | commands = flake8 --ignore=W391 {toxinidir}/statsd {toxinidir}/tests
19 |
20 | [testenv:docs]
21 | deps = sphinx
22 | basepython=python
23 | changedir=docs
24 | commands=
25 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
26 |
27 |
--------------------------------------------------------------------------------
/docs/_theme/wolph/relations.html:
--------------------------------------------------------------------------------
1 |
Related Topics
2 |
20 |
--------------------------------------------------------------------------------
/statsd/compat.py:
--------------------------------------------------------------------------------
1 | '''
2 | Compatibility library for python2 and python3 support.
3 | '''
4 | import sys
5 | import decimal
6 |
7 | PY3K = sys.version_info >= (3, 0)
8 |
9 |
10 | def iter_dict(dict_): # pragma: no cover
11 | if PY3K:
12 | return dict_.items()
13 | else:
14 | return dict_.iteritems()
15 |
16 |
17 | def to_str(value): # pragma: no cover
18 | if PY3K and isinstance(value, bytes):
19 | value = value.encode('utf-8', 'replace')
20 | elif not PY3K and isinstance(value, unicode):
21 | value = value.encode('utf-8', 'replace')
22 | return value
23 |
24 | if PY3K: # pragma: no cover
25 | NUM_TYPES = int, float, decimal.Decimal
26 | else: # pragma: no cover
27 | NUM_TYPES = int, long, float, decimal.Decimal
28 |
--------------------------------------------------------------------------------
/tests/test_client.py:
--------------------------------------------------------------------------------
1 | import statsd
2 | from unittest import TestCase
3 |
4 |
5 | class TestClient(TestCase):
6 |
7 | def test_average_shortcut(self):
8 | average = statsd.Client('average').get_average()
9 | assert isinstance(average, statsd.Average)
10 |
11 | def test_counter_shortcut(self):
12 | counter = statsd.Client('counter').get_counter()
13 | assert isinstance(counter, statsd.Counter)
14 |
15 | def test_gauge_shortcut(self):
16 | gauge = statsd.Client('gauge').get_gauge()
17 | assert isinstance(gauge, statsd.Gauge)
18 |
19 | def test_raw_shortcut(self):
20 | raw = statsd.Client('raw').get_raw()
21 | assert isinstance(raw, statsd.Raw)
22 |
23 | def test_timer_shortcut(self):
24 | timer = statsd.Client('timer').get_timer()
25 | assert isinstance(timer, statsd.Timer)
26 |
--------------------------------------------------------------------------------
/statsd/average.py:
--------------------------------------------------------------------------------
1 | import statsd
2 |
3 |
4 | class Average(statsd.Client):
5 | '''Class to implement a statsd "average" message.
6 | This value will be averaged against other messages before being
7 | sent.
8 |
9 | See https://github.com/chuyskywalker/statsd/blob/master/README.md for
10 | more info.
11 |
12 | >>> average = Average('application_name')
13 | >>> # do something here
14 | >>> average.send('subname', 123)
15 | True
16 | '''
17 |
18 | def send(self, subname, value):
19 | '''Send the data to statsd via self.connection
20 |
21 | :keyword subname: The subname to report the data to (appended to the
22 | client name)
23 | :keyword value: The raw value to send
24 | '''
25 | name = self._get_name(self.name, subname)
26 | self.logger.info('%s: %d', name, value)
27 | return statsd.Client._send(self, {name: '%d|a' % value})
28 |
29 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Python Statsd Client
2 | =========================================
3 |
4 | `statsd` is a client for Etsy's statsd server, a front end/proxy for the
5 | Graphite stats collection and graphing server.
6 |
7 | Links
8 | -----
9 |
10 | - The source: https://github.com/WoLpH/python-statsd
11 | - Project page: https://pypi.python.org/pypi/python-statsd
12 | - Reporting bugs: https://github.com/WoLpH/python-statsd/issues
13 | - Documentation: http://python-statsd.readthedocs.io/en/latest/
14 | - My blog: http://w.wol.ph/
15 | - Statsd: https://github.com/etsy/statsd
16 | - Graphite: http://graphite.wikidot.com
17 |
18 | API
19 | ---
20 |
21 | .. toctree::
22 | :maxdepth: 2
23 |
24 | statsd.connection
25 | statsd.client
26 | statsd.timer
27 | statsd.counter
28 | statsd.gauge
29 | statsd.raw
30 |
31 | .. include :: ../README.rst
32 |
33 | Indices and tables
34 | ==================
35 |
36 | * :ref:`genindex`
37 | * :ref:`search`
38 |
39 |
--------------------------------------------------------------------------------
/docs/_theme/wolph/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "basic/layout.html" %}
2 | {%- block extrahead %}
3 | {{ super() }}
4 | {% if theme_touch_icon %}
5 |
6 | {% endif %}
7 |
9 | {% endblock %}
10 | {%- block relbar2 %}{% endblock %}
11 | {%- block footer %}
12 |
22 |
23 |
27 | {%- endblock %}
28 |
--------------------------------------------------------------------------------
/statsd/__init__.py:
--------------------------------------------------------------------------------
1 | from statsd.connection import Connection
2 | from statsd.client import Client
3 | from statsd.timer import Timer
4 | from statsd.gauge import Gauge
5 | from statsd.average import Average
6 | from statsd.raw import Raw
7 | from statsd.counter import Counter, increment, decrement
8 |
9 | __all__ = [
10 | 'Client',
11 | 'Connection',
12 | 'Timer',
13 | 'Counter',
14 | 'Gauge',
15 | 'Average',
16 | 'Raw',
17 | 'increment',
18 | 'decrement',
19 | ]
20 |
21 |
22 | # The doctests in this package, when run, will try to send data on the wire.
23 | # To keep this from happening, we hook into nose's machinery to mock out
24 | # `Connection.send` at the beginning of testing this package, and reset it at
25 | # the end.
26 | _connection_patch = None
27 |
28 |
29 | def setup_package():
30 | # Since we don't want mock to be a global requirement, we need the import
31 | # the setup method.
32 | import mock
33 | global _connection_patch
34 | _connection_patch = mock.patch('statsd.Connection.send')
35 |
36 | send = _connection_patch.start()
37 | send.return_value = True
38 |
39 |
40 | def teardown_package():
41 | assert _connection_patch
42 | _connection_patch.stop()
43 |
44 |
--------------------------------------------------------------------------------
/docs/_theme/wolph/static/small_flask.css:
--------------------------------------------------------------------------------
1 | /*
2 | * small_flask.css_t
3 | * ~~~~~~~~~~~~~~~~~
4 | *
5 | * :copyright: Copyright 2010 by Armin Ronacher.
6 | * :license: Flask Design License, see LICENSE for details.
7 | */
8 |
9 | body {
10 | margin: 0;
11 | padding: 20px 30px;
12 | }
13 |
14 | div.documentwrapper {
15 | float: none;
16 | background: white;
17 | }
18 |
19 | div.sphinxsidebar {
20 | display: block;
21 | float: none;
22 | width: 102.5%;
23 | margin: 50px -30px -20px -30px;
24 | padding: 10px 20px;
25 | background: #333;
26 | color: white;
27 | }
28 |
29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
30 | div.sphinxsidebar h3 a {
31 | color: white;
32 | }
33 |
34 | div.sphinxsidebar a {
35 | color: #aaa;
36 | }
37 |
38 | div.sphinxsidebar p.logo {
39 | display: none;
40 | }
41 |
42 | div.document {
43 | width: 100%;
44 | margin: 0;
45 | }
46 |
47 | div.related {
48 | display: block;
49 | margin: 0;
50 | padding: 10px 0 20px 0;
51 | }
52 |
53 | div.related ul,
54 | div.related ul li {
55 | margin: 0;
56 | padding: 0;
57 | }
58 |
59 | div.footer {
60 | display: none;
61 | }
62 |
63 | div.bodywrapper {
64 | margin: 0;
65 | }
66 |
67 | div.body {
68 | min-height: 0;
69 | padding: 0;
70 | }
71 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import setuptools
3 |
4 | # To prevent importing about and thereby breaking the coverage info we use this
5 | # exec hack
6 | about = {}
7 | with open('statsd/__about__.py') as fp:
8 | exec(fp.read(), about)
9 |
10 | if os.path.isfile('README.rst'):
11 | long_description = open('README.rst').read()
12 | else:
13 | long_description = 'See http://pypi.python.org/pypi/python-statsd/'
14 |
15 | tests_require = [
16 | 'nose',
17 | 'coverage',
18 | 'mock',
19 | ]
20 |
21 | docs_require = [
22 | 'changelog',
23 | 'sphinx>=1.5.0',
24 | ]
25 |
26 | if __name__ == '__main__':
27 | setuptools.setup(
28 | name=about['__package_name__'],
29 | version=about['__version__'],
30 | author=about['__author__'],
31 | author_email=about['__author_email__'],
32 | description=about['__description__'],
33 | url=about['__url__'],
34 | license='BSD',
35 | packages=setuptools.find_packages(exclude=('docs', 'tests',)),
36 | long_description=long_description,
37 | test_suite='nose.collector',
38 | classifiers=[
39 | 'License :: OSI Approved :: BSD License',
40 | ],
41 | extras_require={
42 | 'docs': docs_require,
43 | 'tests': tests_require,
44 | },
45 | )
46 |
47 |
--------------------------------------------------------------------------------
/statsd/raw.py:
--------------------------------------------------------------------------------
1 | import statsd
2 | import datetime as dt
3 |
4 |
5 | class Raw(statsd.Client):
6 | '''Class to implement a statsd raw message.
7 | If a service has already summarized its own
8 | data for e.g. inspection purposes, use this
9 | summarized data to send to a statsd that has
10 | the raw patch, and this data will be sent
11 | to graphite pretty much unchanged.
12 |
13 | See https://github.com/chuyskywalker/statsd/blob/master/README.md for
14 | more info.
15 |
16 | >>> raw = Raw('test')
17 | >>> raw.send('name', 12435)
18 | True
19 | >>> import time
20 | >>> raw.send('name', 12435, time.time())
21 | True
22 | '''
23 |
24 | def send(self, subname, value, timestamp=None):
25 | '''Send the data to statsd via self.connection
26 |
27 | :keyword subname: The subname to report the data to (appended to the
28 | client name)
29 | :type subname: str
30 | :keyword value: The raw value to send
31 | '''
32 | if timestamp is None:
33 | ts = int(dt.datetime.now().strftime("%s"))
34 | else:
35 | ts = timestamp
36 | name = self._get_name(self.name, subname)
37 | self.logger.info('%s: %s %s' % (name, value, ts))
38 | return statsd.Client._send(self, {name: '%s|r|%s' % (value, ts)})
39 |
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Rick van Hattem
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | Redistributions in binary form must reproduce the above copyright notice, this
10 | list of conditions and the following disclaimer in the documentation and/or
11 | other materials provided with the distribution.
12 | Neither the name of the nor the names of its contributors may be
13 | used to endorse or promote products derived from this software without specific
14 | prior written permission.
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
24 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
26 |
--------------------------------------------------------------------------------
/tests/test_gauge.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from unittest import TestCase
3 | from decimal import Decimal
4 | import mock
5 | import statsd
6 |
7 |
8 | class TestGauge(TestCase):
9 |
10 | def setUp(self):
11 | self.gauge = statsd.Gauge('testing')
12 |
13 | def test_send_float(self):
14 | with mock.patch('statsd.Client') as mock_client:
15 | self.gauge.send('', 10.5)
16 | mock_client._send.assert_called_with(mock.ANY,
17 | {'testing': '10.5|g'})
18 |
19 | def test_send_decimal(self):
20 | with mock.patch('statsd.Client') as mock_client:
21 | self.gauge.send('', Decimal('6.576'))
22 | mock_client._send.assert_called_with(mock.ANY,
23 | {'testing': '6.576|g'})
24 |
25 | def test_send_integer(self):
26 | with mock.patch('statsd.Client') as mock_client:
27 | self.gauge.send('', 1)
28 | mock_client._send.assert_called_with(mock.ANY,
29 | {'testing': '1|g'})
30 |
31 | def test_set(self):
32 | with mock.patch('statsd.Client') as mock_client:
33 | self.gauge.set('', -1)
34 | mock_client._send.assert_any_call(mock.ANY, {'testing': '0|g'})
35 | mock_client._send.assert_any_call(mock.ANY, {'testing': '-1|g'})
36 | mock_client.reset_mock()
37 | self.gauge.set('', 1)
38 | mock_client._send.assert_called_with(mock.ANY, {'testing': '1|g'})
39 |
--------------------------------------------------------------------------------
/tests/test_counter.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from unittest import TestCase
3 | import mock
4 | import statsd
5 |
6 |
7 | class TestCounter(TestCase):
8 |
9 | def setUp(self):
10 | self.counter = statsd.Counter('testing')
11 |
12 | def test_increment(self):
13 | with mock.patch('statsd.Client') as mock_client:
14 | self.counter.increment('')
15 | mock_client._send.assert_called_with(mock.ANY, {'testing': '1|c'})
16 |
17 | self.counter.increment('', 2)
18 | mock_client._send.assert_called_with(mock.ANY, {'testing': '2|c'})
19 |
20 | self.counter += 3
21 | mock_client._send.assert_called_with(mock.ANY, {'testing': '3|c'})
22 |
23 | statsd.increment('testing', 4)
24 | mock_client._send.assert_called_with(mock.ANY, {'testing': '4|c'})
25 |
26 | statsd.increment('testing')
27 | mock_client._send.assert_called_with(mock.ANY, {'testing': '1|c'})
28 |
29 | def test_decrement(self):
30 | with mock.patch('statsd.Client') as mock_client:
31 | self.counter.decrement('')
32 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-1|c'})
33 |
34 | self.counter.decrement('', 2)
35 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-2|c'})
36 |
37 | self.counter -= 3
38 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-3|c'})
39 |
40 | statsd.decrement('testing', 4)
41 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-4|c'})
42 |
43 | statsd.decrement('testing')
44 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-1|c'})
45 |
46 | def test_decrement_with_an_int(self):
47 | with mock.patch('statsd.Client') as mock_client:
48 | self.counter.decrement('', 2)
49 | mock_client._send.assert_called_with(mock.ANY, {'testing': '-2|c'})
50 |
--------------------------------------------------------------------------------
/docs/_theme/LICENSE:
--------------------------------------------------------------------------------
1 | Modifications:
2 |
3 | Copyright (c) 2012 Rick van Hattem.
4 |
5 |
6 | Original Projects:
7 |
8 | Copyright (c) 2010 Kenneth Reitz.
9 | Copyright (c) 2010 by Armin Ronacher.
10 |
11 |
12 | Some rights reserved.
13 |
14 | Redistribution and use in source and binary forms of the theme, with or
15 | without modification, are permitted provided that the following conditions
16 | are met:
17 |
18 | * Redistributions of source code must retain the above copyright
19 | notice, this list of conditions and the following disclaimer.
20 |
21 | * Redistributions in binary form must reproduce the above
22 | copyright notice, this list of conditions and the following
23 | disclaimer in the documentation and/or other materials provided
24 | with the distribution.
25 |
26 | * The names of the contributors may not be used to endorse or
27 | promote products derived from this software without specific
28 | prior written permission.
29 |
30 | We kindly ask you to only use these themes in an unmodified manner just
31 | for Flask and Flask-related products, not for unrelated projects. If you
32 | like the visual style and want to use it for your own projects, please
33 | consider making some larger changes to the themes (such as changing
34 | font faces, sizes, colors or margins).
35 |
36 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
37 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
38 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
40 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
41 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
42 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
44 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
46 | POSSIBILITY OF SUCH DAMAGE.
47 |
--------------------------------------------------------------------------------
/tests/test_connection.py:
--------------------------------------------------------------------------------
1 | import mock
2 | import statsd
3 | import unittest
4 |
5 |
6 | class ConnectionException(Exception):
7 |
8 | pass
9 |
10 |
11 | class TestConnection(unittest.TestCase):
12 |
13 | def test_set_disabled_to_false_by_default(self):
14 | result = statsd.connection.Connection()
15 | assert result._disabled is False
16 |
17 | def test_send_returns_false_if_disabled(self):
18 | connection = statsd.connection.Connection(disabled=True)
19 | assert connection.send({'data': True}) is False
20 | assert connection.send({'data': True}, 1) is False
21 |
22 | @mock.patch('socket.socket')
23 | def test_send_returns_true_if_enabled(self, mock_class):
24 | connection = statsd.connection.Connection()
25 | assert connection.send({'data': True}) is True
26 | assert connection.send({'test:1|c': True}, 0.99999999)
27 | assert connection.send({'test:1|c': True}, 0.00000001)
28 | assert connection.send({'data': True}, 1) is True
29 |
30 | def test_send_exception(self, mock_class=None):
31 | connection = statsd.connection.Connection()
32 | socket = mock.MagicMock()
33 | send = mock.PropertyMock(side_effect=ConnectionException)
34 | type(socket).send = send
35 | connection.udp_sock = socket
36 | assert not connection.send({'data': True})
37 |
38 | def test_connection_set_defaults(self):
39 | connection = statsd.connection.Connection()
40 | assert connection._host == 'localhost'
41 | assert connection._port == 8125
42 | assert connection._sample_rate == 1
43 | assert connection._disabled is False
44 |
45 | statsd.connection.Connection.set_defaults('127.0.0.1', 1234, 10, True)
46 | connection = statsd.connection.Connection()
47 | assert connection._host == '127.0.0.1'
48 | assert connection._port == 1234
49 | assert connection._sample_rate == 10
50 | assert connection._disabled is True
51 |
52 | statsd.connection.Connection.set_defaults()
53 | connection = statsd.connection.Connection()
54 | assert connection._host == 'localhost'
55 | assert connection._port == 8125
56 | assert connection._sample_rate == 1
57 | assert connection._disabled is False
58 |
59 | def test_repr(self):
60 | connection = statsd.connection.Connection()
61 | assert '' == repr(connection)
62 |
63 |
64 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 |
15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " pickle to make pickle files"
22 | @echo " json to make JSON files"
23 | @echo " htmlhelp to make HTML files and a HTML help project"
24 | @echo " qthelp to make HTML files and a qthelp project"
25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
26 | @echo " changes to make an overview of all changed/added/deprecated items"
27 | @echo " linkcheck to check all external links for integrity"
28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
29 |
30 | clean:
31 | -rm -rf $(BUILDDIR)/*
32 |
33 | html:
34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
35 | @echo
36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
37 |
38 | dirhtml:
39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
40 | @echo
41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
42 |
43 | pickle:
44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
45 | @echo
46 | @echo "Build finished; now you can process the pickle files."
47 |
48 | json:
49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
50 | @echo
51 | @echo "Build finished; now you can process the JSON files."
52 |
53 | htmlhelp:
54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
55 | @echo
56 | @echo "Build finished; now you can run HTML Help Workshop with the" \
57 | ".hhp project file in $(BUILDDIR)/htmlhelp."
58 |
59 | qthelp:
60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
61 | @echo
62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonStatsd.qhcp"
65 | @echo "To view the help file:"
66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonStatsd.qhc"
67 |
68 | latex:
69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
70 | @echo
71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
73 | "run these through (pdf)latex."
74 |
75 | changes:
76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
77 | @echo
78 | @echo "The overview file is in $(BUILDDIR)/changes."
79 |
80 | linkcheck:
81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
82 | @echo
83 | @echo "Link check complete; look for any errors in the above output " \
84 | "or in $(BUILDDIR)/linkcheck/output.txt."
85 |
86 | doctest:
87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
88 | @echo "Testing of doctests in the sources finished, look at the " \
89 | "results in $(BUILDDIR)/doctest/output.txt."
90 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | set SPHINXBUILD=sphinx-build
6 | set BUILDDIR=build
7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
8 | if NOT "%PAPER%" == "" (
9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
10 | )
11 |
12 | if "%1" == "" goto help
13 |
14 | if "%1" == "help" (
15 | :help
16 | echo.Please use `make ^` where ^ is one of
17 | echo. html to make standalone HTML files
18 | echo. dirhtml to make HTML files named index.html in directories
19 | echo. pickle to make pickle files
20 | echo. json to make JSON files
21 | echo. htmlhelp to make HTML files and a HTML help project
22 | echo. qthelp to make HTML files and a qthelp project
23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
24 | echo. changes to make an overview over all changed/added/deprecated items
25 | echo. linkcheck to check all external links for integrity
26 | echo. doctest to run all doctests embedded in the documentation if enabled
27 | goto end
28 | )
29 |
30 | if "%1" == "clean" (
31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
32 | del /q /s %BUILDDIR%\*
33 | goto end
34 | )
35 |
36 | if "%1" == "html" (
37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
38 | echo.
39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
40 | goto end
41 | )
42 |
43 | if "%1" == "dirhtml" (
44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
47 | goto end
48 | )
49 |
50 | if "%1" == "pickle" (
51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
52 | echo.
53 | echo.Build finished; now you can process the pickle files.
54 | goto end
55 | )
56 |
57 | if "%1" == "json" (
58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
59 | echo.
60 | echo.Build finished; now you can process the JSON files.
61 | goto end
62 | )
63 |
64 | if "%1" == "htmlhelp" (
65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
66 | echo.
67 | echo.Build finished; now you can run HTML Help Workshop with the ^
68 | .hhp project file in %BUILDDIR%/htmlhelp.
69 | goto end
70 | )
71 |
72 | if "%1" == "qthelp" (
73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
74 | echo.
75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
76 | .qhcp project file in %BUILDDIR%/qthelp, like this:
77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonStatsd.qhcp
78 | echo.To view the help file:
79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonStatsd.ghc
80 | goto end
81 | )
82 |
83 | if "%1" == "latex" (
84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
85 | echo.
86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
87 | goto end
88 | )
89 |
90 | if "%1" == "changes" (
91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
92 | echo.
93 | echo.The overview file is in %BUILDDIR%/changes.
94 | goto end
95 | )
96 |
97 | if "%1" == "linkcheck" (
98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
99 | echo.
100 | echo.Link check complete; look for any errors in the above output ^
101 | or in %BUILDDIR%/linkcheck/output.txt.
102 | goto end
103 | )
104 |
105 | if "%1" == "doctest" (
106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
107 | echo.
108 | echo.Testing of doctests in the sources finished, look at the ^
109 | results in %BUILDDIR%/doctest/output.txt.
110 | goto end
111 | )
112 |
113 | :end
114 |
--------------------------------------------------------------------------------
/statsd/connection.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import socket
3 | import random
4 |
5 | from . import compat
6 |
7 |
8 | class Connection(object):
9 | '''Statsd Connection
10 |
11 | :keyword host: The statsd host to connect to, defaults to `localhost`
12 | :type host: str
13 | :keyword port: The statsd port to connect to, defaults to `8125`
14 | :type port: int
15 | :keyword sample_rate: The sample rate, defaults to `1` (meaning always)
16 | :type sample_rate: int
17 | :keyword disabled: Turn off sending UDP packets, defaults to ``False``
18 | :type disabled: bool
19 | '''
20 |
21 | default_host = 'localhost'
22 | default_port = 8125
23 | default_sample_rate = 1
24 | default_disabled = False
25 |
26 | @classmethod
27 | def set_defaults(
28 | cls, host='localhost', port=8125, sample_rate=1, disabled=False):
29 | cls.default_host = host
30 | cls.default_port = port
31 | cls.default_sample_rate = sample_rate
32 | cls.default_disabled = disabled
33 |
34 | def __init__(self, host=None, port=None, sample_rate=None, disabled=None):
35 | self._host = host or self.default_host
36 | self._port = int(port or self.default_port)
37 | self._sample_rate = sample_rate or self.default_sample_rate
38 | self._disabled = disabled or self.default_disabled
39 | self.logger = logging.getLogger(
40 | '%s.%s' % (__name__, self.__class__.__name__))
41 | self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
42 | self.udp_sock.connect((self._host, self._port))
43 | self.logger.debug(
44 | 'Initialized connection to %s:%d with P(%.1f)',
45 | self._host, self._port, self._sample_rate)
46 |
47 | def send(self, data, sample_rate=None):
48 | '''Send the data over UDP while taking the sample_rate in account
49 |
50 | The sample rate should be a number between `0` and `1` which indicates
51 | the probability that a message will be sent. The sample_rate is also
52 | communicated to `statsd` so it knows what multiplier to use.
53 |
54 | :keyword data: The data to send
55 | :type data: dict
56 | :keyword sample_rate: The sample rate, defaults to `1` (meaning always)
57 | :type sample_rate: int
58 | '''
59 | if self._disabled:
60 | self.logger.debug('Connection disabled, not sending data')
61 | return False
62 | if sample_rate is None:
63 | sample_rate = self._sample_rate
64 |
65 | sampled_data = {}
66 | if sample_rate < 1:
67 | if random.random() <= sample_rate:
68 | # Modify the data so statsd knows our sample_rate
69 | for stat, value in compat.iter_dict(data):
70 | sampled_data[stat] = '%s|@%s' % (data[stat], sample_rate)
71 | else:
72 | sampled_data = data
73 |
74 | try:
75 | for stat, value in compat.iter_dict(sampled_data):
76 | send_data = ('%s:%s' % (stat, value)).encode("utf-8")
77 | self.udp_sock.send(send_data)
78 | return True
79 | except Exception as e:
80 | self.logger.exception('unexpected error %r while sending data', e)
81 | return False
82 |
83 | def __del__(self):
84 | '''
85 | We close UDP socket connection explicitly for pypy.
86 | '''
87 | self.udp_sock.close() # pragma: no cover
88 |
89 | def __repr__(self):
90 | return '<%s[%s:%d] P(%.1f)>' % (
91 | self.__class__.__name__,
92 | self._host,
93 | self._port,
94 | self._sample_rate,
95 | )
96 |
97 |
--------------------------------------------------------------------------------
/statsd/counter.py:
--------------------------------------------------------------------------------
1 | import statsd
2 |
3 |
4 | class Counter(statsd.Client):
5 | '''Class to implement a statd counter
6 |
7 | Additional documentation is available at the
8 | parent class :class:`~statsd.client.Client`
9 |
10 | The values can be incremented/decremented by using either the
11 | `increment()` and `decrement()` methods or by simply adding/deleting from
12 | the object.
13 |
14 | >>> counter = Counter('application_name')
15 | >>> counter += 10
16 |
17 | >>> counter = Counter('application_name')
18 | >>> counter -= 10
19 | '''
20 |
21 | def _send(self, subname, delta):
22 | '''Send the data to statsd via self.connection
23 |
24 | :keyword subname: The subname to report the data to (appended to the
25 | client name)
26 | :type subname: str
27 | :keyword delta: The delta to add to/remove from the counter
28 | :type delta: int
29 | '''
30 | name = self._get_name(self.name, subname)
31 | self.logger.info('%s: %d', name, delta)
32 | return statsd.Client._send(self, {name: '%d|c' % delta})
33 |
34 | def increment(self, subname=None, delta=1):
35 | '''Increment the counter with `delta`
36 |
37 | :keyword subname: The subname to report the data to (appended to the
38 | client name)
39 | :type subname: str
40 | :keyword delta: The delta to add to the counter
41 | :type delta: int
42 |
43 | >>> counter = Counter('application_name')
44 | >>> counter.increment('counter_name', 10)
45 | True
46 | >>> counter.increment(delta=10)
47 | True
48 | >>> counter.increment('counter_name')
49 | True
50 | '''
51 | return self._send(subname, int(delta))
52 |
53 | def decrement(self, subname=None, delta=1):
54 | '''Decrement the counter with `delta`
55 |
56 | :keyword subname: The subname to report the data to (appended to the
57 | client name)
58 | :type subname: str
59 | :keyword delta: The delta to remove from the counter
60 | :type delta: int
61 |
62 | >>> counter = Counter('application_name')
63 | >>> counter.decrement('counter_name', 10)
64 | True
65 | >>> counter.decrement(delta=10)
66 | True
67 | >>> counter.decrement('counter_name')
68 | True
69 | '''
70 | return self._send(subname, -int(delta))
71 |
72 | def __add__(self, delta):
73 | '''Increment the counter with `delta`
74 |
75 | :keyword delta: The delta to add to the counter
76 | :type delta: int
77 | '''
78 | self.increment(delta=delta)
79 | return self
80 |
81 | def __sub__(self, delta):
82 | '''Decrement the counter with `delta`
83 |
84 | :keyword delta: The delta to remove from the counter
85 | :type delta: int
86 | '''
87 | self.decrement(delta=delta)
88 | return self
89 |
90 |
91 | def increment(key, delta=1):
92 | '''Increment the counter with `delta`
93 |
94 | :keyword key: The key to report the data to
95 | :type key: str
96 | :keyword delta: The delta to add to the counter
97 | :type delta: int
98 | '''
99 | return Counter(key).increment(delta=delta)
100 |
101 |
102 | def decrement(key, delta=1):
103 | '''Decrement the counter with `delta`
104 |
105 | :keyword key: The key to report the data to
106 | :type key: str
107 | :keyword delta: The delta to remove from the counter
108 | :type delta: int
109 | '''
110 | return Counter(key).decrement(delta=delta)
111 |
112 |
--------------------------------------------------------------------------------
/statsd/gauge.py:
--------------------------------------------------------------------------------
1 | import statsd
2 |
3 | from . import compat
4 |
5 |
6 | class Gauge(statsd.Client):
7 |
8 | 'Class to implement a statsd gauge'
9 |
10 | def _send(self, subname, value):
11 | '''Send the data to statsd via self.connection
12 |
13 | :keyword subname: The subname to report the data to (appended to the
14 | client name)
15 | :type subname: str
16 | :keyword value: The gauge value to send
17 | '''
18 | name = self._get_name(self.name, subname)
19 | self.logger.info('%s: %s', name, value)
20 | return statsd.Client._send(self, {name: '%s|g' % value})
21 |
22 | def send(self, subname, value):
23 | '''Send the data to statsd via self.connection
24 |
25 | :keyword subname: The subname to report the data to (appended to the
26 | client name)
27 | :type subname: str
28 | :keyword value: The gauge value to send
29 | '''
30 | assert isinstance(value, compat.NUM_TYPES)
31 | return self._send(subname, value)
32 |
33 | def increment(self, subname=None, delta=1):
34 | '''Increment the gauge with `delta`
35 |
36 | :keyword subname: The subname to report the data to (appended to the
37 | client name)
38 | :type subname: str
39 | :keyword delta: The delta to add to the gauge
40 | :type delta: int
41 |
42 | >>> gauge = Gauge('application_name')
43 | >>> gauge.increment('gauge_name', 10)
44 | True
45 | >>> gauge.increment(delta=10)
46 | True
47 | >>> gauge.increment('gauge_name')
48 | True
49 | '''
50 | delta = int(delta)
51 | sign = "+" if delta >= 0 else ""
52 | return self._send(subname, "%s%d" % (sign, delta))
53 |
54 | def decrement(self, subname=None, delta=1):
55 | '''Decrement the gauge with `delta`
56 |
57 | :keyword subname: The subname to report the data to (appended to the
58 | client name)
59 | :type subname: str
60 | :keyword delta: The delta to remove from the gauge
61 | :type delta: int
62 |
63 | >>> gauge = Gauge('application_name')
64 | >>> gauge.decrement('gauge_name', 10)
65 | True
66 | >>> gauge.decrement(delta=10)
67 | True
68 | >>> gauge.decrement('gauge_name')
69 | True
70 | '''
71 | delta = -int(delta)
72 | sign = "+" if delta >= 0 else ""
73 | return self._send(subname, "%s%d" % (sign, delta))
74 |
75 | def __add__(self, delta):
76 | '''Increment the gauge with `delta`
77 |
78 | :keyword delta: The delta to add to the gauge
79 | :type delta: int
80 |
81 | >>> gauge = Gauge('application_name')
82 | >>> gauge += 5
83 | '''
84 | self.increment(delta=delta)
85 | return self
86 |
87 | def __sub__(self, delta):
88 | '''Decrement the gauge with `delta`
89 |
90 | :keyword delta: The delta to remove from the gauge
91 | :type delta: int
92 |
93 | >>> gauge = Gauge('application_name')
94 | >>> gauge -= 5
95 | '''
96 | self.decrement(delta=delta)
97 | return self
98 |
99 | def set(self, subname, value):
100 | '''
101 | Set the data ignoring the sign, ie set("test", -1) will set "test"
102 | exactly to -1 (not decrement it by 1)
103 |
104 | See https://github.com/etsy/statsd/blob/master/docs/metric_types.md
105 | "Adding a sign to the gauge value will change the value, rather
106 | than setting it.
107 |
108 | gaugor:-10|g
109 | gaugor:+4|g
110 |
111 | So if gaugor was 333, those commands would set it to 333 - 10 + 4, or
112 | 327.
113 |
114 | Note: This implies you can't explicitly set a gauge to a negative
115 | number without first setting it to zero."
116 |
117 | :keyword subname: The subname to report the data to (appended to the
118 | client name)
119 | :type subname: str
120 | :keyword value: The new gauge value
121 | '''
122 |
123 | assert isinstance(value, compat.NUM_TYPES)
124 | if value < 0:
125 | self._send(subname, 0)
126 | return self._send(subname, value)
127 |
--------------------------------------------------------------------------------
/statsd/client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import statsd
3 |
4 | from . import compat
5 |
6 |
7 | class Client(object):
8 |
9 | '''Statsd Client Object
10 |
11 | :keyword name: The name for this client
12 | :type name: str
13 | :keyword connection: The connection to use, will be automatically created
14 | if not given
15 | :type connection: :class:`~statsd.connection.Connection`
16 |
17 | >>> client = Client('test')
18 | >>> client
19 | >
20 | >>> client.get_client('spam')
21 | >
22 | '''
23 |
24 | #: The name of the client, everything sent from this client will be \
25 | #: prefixed by name
26 | name = None
27 |
28 | #: The :class:`~statsd.connection.Connection` to use, creates a new
29 | #: connection if no connection is given
30 | connection = None
31 |
32 | def __init__(self, name, connection=None):
33 | self.name = self._get_name(name)
34 | if not connection:
35 | connection = statsd.Connection()
36 | self.connection = connection
37 | self.logger = logging.getLogger(
38 | '%s.%s' % (__name__, self.__class__.__name__))
39 |
40 | @classmethod
41 | def _get_name(cls, *name_parts):
42 | name_parts = [compat.to_str(x) for x in name_parts if x]
43 | return '.'.join(name_parts)
44 |
45 | def get_client(self, name=None, class_=None):
46 | '''Get a (sub-)client with a separate namespace
47 | This way you can create a global/app based client with subclients
48 | per class/function
49 |
50 | :keyword name: The name to use, if the name for this client was `spam`
51 | and the `name` argument is `eggs` than the resulting name will be
52 | `spam.eggs`
53 | :type name: str
54 | :keyword class_: The :class:`~statsd.client.Client` subclass to use
55 | (e.g. :class:`~statsd.timer.Timer` or
56 | :class:`~statsd.counter.Counter`)
57 | :type class_: :class:`~statsd.client.Client`
58 | '''
59 |
60 | # If the name was given, use it. Otherwise simply clone
61 | name = self._get_name(self.name, name)
62 |
63 | # Create using the given class, or the current class
64 | if not class_:
65 | class_ = self.__class__
66 |
67 | return class_(
68 | name=name,
69 | connection=self.connection,
70 | )
71 |
72 | def get_average(self, name=None):
73 | '''Shortcut for getting an :class:`~statsd.average.Average` instance
74 |
75 | :keyword name: See :func:`~statsd.client.Client.get_client`
76 | :type name: str
77 | '''
78 | return self.get_client(name=name, class_=statsd.Average)
79 |
80 | def get_counter(self, name=None):
81 | '''Shortcut for getting a :class:`~statsd.counter.Counter` instance
82 |
83 | :keyword name: See :func:`~statsd.client.Client.get_client`
84 | :type name: str
85 | '''
86 | return self.get_client(name=name, class_=statsd.Counter)
87 |
88 | def get_gauge(self, name=None):
89 | '''Shortcut for getting a :class:`~statsd.gauge.Gauge` instance
90 |
91 | :keyword name: See :func:`~statsd.client.Client.get_client`
92 | :type name: str
93 | '''
94 | return self.get_client(name=name, class_=statsd.Gauge)
95 |
96 | def get_raw(self, name=None):
97 | '''Shortcut for getting a :class:`~statsd.raw.Raw` instance
98 |
99 | :keyword name: See :func:`~statsd.client.Client.get_client`
100 | :type name: str
101 | '''
102 | return self.get_client(name=name, class_=statsd.Raw)
103 |
104 | def get_timer(self, name=None):
105 | '''Shortcut for getting a :class:`~statsd.timer.Timer` instance
106 |
107 | :keyword name: See :func:`~statsd.client.Client.get_client`
108 | :type name: str
109 | '''
110 | return self.get_client(name=name, class_=statsd.Timer)
111 |
112 | def __repr__(self):
113 | return '<%s:%s@%r>' % (
114 | self.__class__.__name__,
115 | self.name,
116 | self.connection,
117 | )
118 |
119 | def _send(self, data):
120 | return self.connection.send(data)
121 |
--------------------------------------------------------------------------------
/docs/_theme/flask_theme_support.py:
--------------------------------------------------------------------------------
1 | # flasky extensions. flasky pygments style based on tango style
2 | from pygments.style import Style
3 | from pygments.token import Keyword, Name, Comment, String, Error, \
4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
5 |
6 |
7 | class FlaskyStyle(Style):
8 | background_color = "#f8f8f8"
9 | default_style = ""
10 |
11 | styles = {
12 | # No corresponding class for the following:
13 | #Text: "", # class: ''
14 | Whitespace: "underline #f8f8f8", # class: 'w'
15 | Error: "#a40000 border:#ef2929", # class: 'err'
16 | Other: "#000000", # class 'x'
17 |
18 | Comment: "italic #8f5902", # class: 'c'
19 | Comment.Preproc: "noitalic", # class: 'cp'
20 |
21 | Keyword: "bold #004461", # class: 'k'
22 | Keyword.Constant: "bold #004461", # class: 'kc'
23 | Keyword.Declaration: "bold #004461", # class: 'kd'
24 | Keyword.Namespace: "bold #004461", # class: 'kn'
25 | Keyword.Pseudo: "bold #004461", # class: 'kp'
26 | Keyword.Reserved: "bold #004461", # class: 'kr'
27 | Keyword.Type: "bold #004461", # class: 'kt'
28 |
29 | Operator: "#582800", # class: 'o'
30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords
31 |
32 | Punctuation: "bold #000000", # class: 'p'
33 |
34 | # because special names such as Name.Class, Name.Function, etc.
35 | # are not recognized as such later in the parsing, we choose them
36 | # to look the same as ordinary variables.
37 | Name: "#000000", # class: 'n'
38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised
39 | Name.Builtin: "#004461", # class: 'nb'
40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
41 | Name.Class: "#000000", # class: 'nc' - to be revised
42 | Name.Constant: "#000000", # class: 'no' - to be revised
43 | Name.Decorator: "#888", # class: 'nd' - to be revised
44 | Name.Entity: "#ce5c00", # class: 'ni'
45 | Name.Exception: "bold #cc0000", # class: 'ne'
46 | Name.Function: "#000000", # class: 'nf'
47 | Name.Property: "#000000", # class: 'py'
48 | Name.Label: "#f57900", # class: 'nl'
49 | Name.Namespace: "#000000", # class: 'nn' - to be revised
50 | Name.Other: "#000000", # class: 'nx'
51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword
52 | Name.Variable: "#000000", # class: 'nv' - to be revised
53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised
54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised
55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
56 |
57 | Number: "#990000", # class: 'm'
58 |
59 | Literal: "#000000", # class: 'l'
60 | Literal.Date: "#000000", # class: 'ld'
61 |
62 | String: "#4e9a06", # class: 's'
63 | String.Backtick: "#4e9a06", # class: 'sb'
64 | String.Char: "#4e9a06", # class: 'sc'
65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment
66 | String.Double: "#4e9a06", # class: 's2'
67 | String.Escape: "#4e9a06", # class: 'se'
68 | String.Heredoc: "#4e9a06", # class: 'sh'
69 | String.Interpol: "#4e9a06", # class: 'si'
70 | String.Other: "#4e9a06", # class: 'sx'
71 | String.Regex: "#4e9a06", # class: 'sr'
72 | String.Single: "#4e9a06", # class: 's1'
73 | String.Symbol: "#4e9a06", # class: 'ss'
74 |
75 | Generic: "#000000", # class: 'g'
76 | Generic.Deleted: "#a40000", # class: 'gd'
77 | Generic.Emph: "italic #000000", # class: 'ge'
78 | Generic.Error: "#ef2929", # class: 'gr'
79 | Generic.Heading: "bold #000080", # class: 'gh'
80 | Generic.Inserted: "#00A000", # class: 'gi'
81 | Generic.Output: "#888", # class: 'go'
82 | Generic.Prompt: "#745334", # class: 'gp'
83 | Generic.Strong: "bold #000000", # class: 'gs'
84 | Generic.Subheading: "bold #800080", # class: 'gu'
85 | Generic.Traceback: "bold #a40000", # class: 'gt'
86 | }
87 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 |
4 | .. image:: https://travis-ci.org/WoLpH/python-statsd.svg?branch=master
5 | :alt: Test Status
6 | :target: https://travis-ci.org/WoLpH/python-statsd
7 |
8 | .. image:: https://coveralls.io/repos/WoLpH/python-statsd/badge.svg?branch=master
9 | :alt: Coverage Status
10 | :target: https://coveralls.io/r/WoLpH/python-statsd?branch=master
11 |
12 | `statsd` is a client for Etsy's statsd server, a front end/proxy for the
13 | Graphite stats collection and graphing server.
14 |
15 | Links
16 | -----
17 |
18 | - The source: https://github.com/WoLpH/python-statsd
19 | - Project page: https://pypi.python.org/pypi/python-statsd
20 | - Reporting bugs: https://github.com/WoLpH/python-statsd/issues
21 | - Documentation: http://python-statsd.readthedocs.io/en/latest/
22 | - My blog: http://w.wol.ph/
23 | - Statsd: https://github.com/etsy/statsd
24 | - Graphite: http://graphite.wikidot.com
25 |
26 | Install
27 | -------
28 |
29 | To install simply execute `python setup.py install`.
30 | If you want to run the tests first, run `python setup.py nosetests`
31 |
32 |
33 | Usage
34 | -----
35 |
36 | To get started real quick, just try something like this:
37 |
38 | Basic Usage
39 | ~~~~~~~~~~~
40 |
41 | Timers
42 | ^^^^^^
43 |
44 | >>> import statsd
45 | >>>
46 | >>> timer = statsd.Timer('MyApplication')
47 | >>>
48 | >>> timer.start()
49 | >>> # do something here
50 | >>> timer.stop('SomeTimer')
51 |
52 |
53 | Counters
54 | ^^^^^^^^
55 |
56 | >>> import statsd
57 | >>>
58 | >>> counter = statsd.Counter('MyApplication')
59 | >>> # do something here
60 | >>> counter += 1
61 |
62 |
63 | Gauge
64 | ^^^^^
65 |
66 | >>> import statsd
67 | >>>
68 | >>> gauge = statsd.Gauge('MyApplication')
69 | >>> # do something here
70 | >>> gauge.send('SomeName', value)
71 |
72 |
73 | Raw
74 | ^^^
75 |
76 | Raw strings should be e.g. pre-summarized data or other data that will
77 | get passed directly to carbon. This can be used as a time and
78 | bandwidth-saving mechanism sending a lot of samples could use a lot of
79 | bandwidth (more b/w is used in udp headers than data for a gauge, for
80 | instance).
81 |
82 |
83 |
84 | >>> import statsd
85 | >>>
86 | >>> raw = statsd.Raw('MyApplication', connection)
87 | >>> # do something here
88 | >>> raw.send('SomeName', value, timestamp)
89 |
90 | The raw type wants to have a timestamp in seconds since the epoch (the
91 | standard unix timestamp, e.g. the output of "date +%s"), but if you leave it out or
92 | provide None it will provide the current time as part of the message
93 |
94 | Average
95 | ^^^^^^^
96 |
97 | >>> import statsd
98 | >>>
99 | >>> average = statsd.Average('MyApplication', connection)
100 | >>> # do something here
101 | >>> average.send('SomeName', 'somekey:%d'.format(value))
102 |
103 |
104 | Connection settings
105 | ^^^^^^^^^^^^^^^^^^^
106 |
107 | If you need some settings other than the defaults for your ``Connection``,
108 | you can use ``Connection.set_defaults()``.
109 |
110 | >>> import statsd
111 | >>> statsd.Connection.set_defaults(host='localhost', port=8125, sample_rate=1, disabled=False)
112 |
113 | Every interaction with statsd after these are set will use whatever you
114 | specify, unless you explicitly create a different ``Connection`` to use
115 | (described below).
116 |
117 | Defaults:
118 |
119 | - ``host`` = ``'localhost'``
120 | - ``port`` = ``8125``
121 | - ``sample_rate`` = ``1``
122 | - ``disabled`` = ``False``
123 |
124 |
125 | Advanced Usage
126 | --------------
127 |
128 | >>> import statsd
129 | >>>
130 | >>> # Open a connection to `server` on port `1234` with a `50%` sample rate
131 | >>> statsd_connection = statsd.Connection(
132 | ... host='server',
133 | ... port=1234,
134 | ... sample_rate=0.5,
135 | ... )
136 | >>>
137 | >>> # Create a client for this application
138 | >>> statsd_client = statsd.Client(__name__, statsd_connection)
139 | >>>
140 | >>> class SomeClass(object):
141 | ... def __init__(self):
142 | ... # Create a client specific for this class
143 | ... self.statsd_client = statsd_client.get_client(
144 | ... self.__class__.__name__)
145 | ...
146 | ... def do_something(self):
147 | ... # Create a `timer` client
148 | ... timer = self.statsd_client.get_client(class_=statsd.Timer)
149 | ...
150 | ... # start the measurement
151 | ... timer.start()
152 | ...
153 | ... # do something
154 | ... timer.intermediate('intermediate_value')
155 | ...
156 | ... # do something else
157 | ... timer.stop('total')
158 |
159 | If there is a need to turn *OFF* the service and avoid sending UDP messages,
160 | the ``Connection`` class can be disabled by enabling the disabled argument::
161 |
162 | >>> statsd_connection = statsd.Connection(
163 | ... host='server',
164 | ... port=1234,
165 | ... sample_rate=0.5,
166 | ... disabled=True
167 | ... )
168 |
169 | If logging's level is set to debug the ``Connection`` object will inform it is
170 | not sending UDP messages anymore.
171 |
--------------------------------------------------------------------------------
/statsd/timer.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import time
3 | from functools import wraps, partial
4 |
5 | import statsd
6 |
7 |
8 | class Timer(statsd.Client):
9 |
10 | '''
11 | Statsd Timer Object
12 |
13 | Additional documentation is available at the parent class
14 | :class:`~statsd.client.Client`
15 |
16 | :keyword name: The name for this timer
17 | :type name: str
18 | :keyword connection: The connection to use, will be automatically created
19 | if not given
20 | :type connection: :class:`~statsd.connection.Connection`
21 | :keyword min_send_threshold: Timings smaller than this will not be sent so
22 | -1 can be used for all.
23 | :type min_send_threshold: int
24 |
25 | >>> timer = Timer('application_name').start()
26 | >>> # do something
27 | >>> timer.stop('executed_action')
28 | True
29 | '''
30 |
31 | def __init__(self, name, connection=None, min_send_threshold=-1):
32 | super(Timer, self).__init__(name, connection=connection)
33 | self._start = None
34 | self._last = None
35 | self._stop = None
36 | self.min_send_threshold = min_send_threshold
37 |
38 | def start(self):
39 | '''Start the timer and store the start time, this can only be executed
40 | once per instance
41 |
42 | It returns the timer instance so it can be chained when instantiating
43 | the timer instance like this:
44 | ``timer = Timer('application_name').start()``'''
45 | assert self._start is None, (
46 | 'Unable to start, the timer is already running')
47 | self._last = self._start = time.time()
48 | return self
49 |
50 | def send(self, subname, delta):
51 | '''Send the data to statsd via self.connection
52 |
53 | :keyword subname: The subname to report the data to (appended to the
54 | client name)
55 | :type subname: str
56 | :keyword delta: The time delta (time.time() - time.time()) to report
57 | :type delta: float
58 | '''
59 | ms = delta * 1000
60 | if ms > self.min_send_threshold:
61 | name = self._get_name(self.name, subname)
62 | self.logger.info('%s: %0.08fms', name, ms)
63 | return statsd.Client._send(self, {name: '%0.08f|ms' % ms})
64 | else:
65 | return True
66 |
67 | def intermediate(self, subname):
68 | '''Send the time that has passed since our last measurement
69 |
70 | :keyword subname: The subname to report the data to (appended to the
71 | client name)
72 | :type subname: str
73 | '''
74 | t = time.time()
75 | response = self.send(subname, t - self._last)
76 | self._last = t
77 | return response
78 |
79 | def stop(self, subname='total'):
80 | '''Stop the timer and send the total since `start()` was run
81 |
82 | :keyword subname: The subname to report the data to (appended to the
83 | client name)
84 | :type subname: str
85 | '''
86 | assert self._stop is None, (
87 | 'Unable to stop, the timer is already stopped')
88 | self._stop = time.time()
89 | return self.send(subname, self._stop - self._start)
90 |
91 | def __enter__(self):
92 | '''
93 | Make a context manager out of self to measure time execution in a block
94 | of code.
95 |
96 | :return: statsd.timer.Timer
97 | '''
98 | self.start()
99 | return self
100 |
101 | def __exit__(self, exc_type, exc_val, exc_tb):
102 | '''
103 | Stop measuring time sending total metric, while exiting block of code.
104 |
105 | :param exc_type:
106 | :param exc_val:
107 | :param exc_tb:
108 | :return:
109 | '''
110 | self.stop()
111 |
112 | def _decorate(self, name, function, class_=None):
113 | class_ = class_ or Timer
114 |
115 | @wraps(function)
116 | def _decorator(*args, **kwargs):
117 | timer = self.get_client(name, class_)
118 | timer.start()
119 | try:
120 | return function(*args, **kwargs)
121 | finally:
122 | # Stop the timer, send the message and cleanup
123 | timer.stop('')
124 |
125 | return _decorator
126 |
127 | def decorate(self, function_or_name):
128 | '''Decorate a function to time the execution
129 |
130 | The method can be called with or without a name. If no name is given
131 | the function defaults to the name of the function.
132 |
133 | :keyword function_or_name: The name to post to or the function to wrap
134 |
135 | >>> from statsd import Timer
136 | >>> timer = Timer('application_name')
137 | >>>
138 | >>> @timer.decorate
139 | ... def some_function():
140 | ... # resulting timer name: application_name.some_function
141 | ... pass
142 | >>>
143 | >>> @timer.decorate('my_timer')
144 | ... def some_other_function():
145 | ... # resulting timer name: application_name.my_timer
146 | ... pass
147 |
148 | '''
149 | if callable(function_or_name):
150 | return self._decorate(function_or_name.__name__, function_or_name)
151 | else:
152 | return partial(self._decorate, function_or_name)
153 |
154 | @contextlib.contextmanager
155 | def time(self, subname=None, class_=None):
156 | '''Returns a context manager to time execution of a block of code.
157 |
158 | :keyword subname: The subname to report data to
159 | :type subname: str
160 | :keyword class_: The :class:`~statsd.client.Client` subclass to use
161 | (e.g. :class:`~statsd.timer.Timer` or
162 | :class:`~statsd.counter.Counter`)
163 | :type class_: :class:`~statsd.client.Client`
164 |
165 | >>> from statsd import Timer
166 | >>> timer = Timer('application_name')
167 | >>>
168 | >>> with timer.time():
169 | ... # resulting timer name: application_name
170 | ... pass
171 | >>>
172 | >>>
173 | >>> with timer.time('context_timer'):
174 | ... # resulting timer name: application_name.context_timer
175 | ... pass
176 |
177 | '''
178 | if class_ is None:
179 | class_ = Timer
180 | timer = self.get_client(subname, class_)
181 | timer.start()
182 | yield
183 | timer.stop('')
184 |
--------------------------------------------------------------------------------
/tests/test_timer.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from unittest import TestCase
3 | import mock
4 | import statsd
5 |
6 |
7 | class TestTimerBase(TestCase):
8 |
9 | def tearDown(self):
10 | self._time_patch.stop()
11 |
12 | def get_time(self, mock_client, key):
13 | return float(self.get_arg(mock_client, key).split('|')[0])
14 |
15 | def get_arg(self, mock_client, key):
16 | return mock_client._send.call_args[0][1][key]
17 |
18 |
19 | class TestTimerDecorator(TestTimerBase):
20 |
21 | def setUp(self):
22 | self.timer = statsd.Timer('timer')
23 |
24 | # get time.time() to always return the same value so that this test
25 | # isn't system load dependant.
26 | self._time_patch = mock.patch('time.time')
27 | time_time = self._time_patch.start()
28 |
29 | def generator():
30 | i = 0.0
31 | while True:
32 | i += 0.1234
33 | yield i
34 | time_time.side_effect = generator()
35 |
36 | @mock.patch('statsd.Client')
37 | def test_decorator_a(self, mock_client):
38 | @self.timer.decorate
39 | def a():
40 | pass
41 |
42 | a()
43 |
44 | assert self.get_time(mock_client, 'timer.a') == 123.4, \
45 | 'This test must execute within 2ms'
46 |
47 | @mock.patch('statsd.Client')
48 | def test_decorator_named_spam(self, mock_client):
49 | @self.timer.decorate('spam')
50 | def a():
51 | pass
52 | a()
53 |
54 | assert self.get_time(mock_client, 'timer.spam') == 123.4, \
55 | 'This test must execute within 2ms'
56 | assert a.__name__ == 'a'
57 |
58 | @mock.patch('statsd.Client')
59 | def test_nested_naming_decorator(self, mock_client):
60 | timer = self.timer.get_client('eggs0')
61 |
62 | @timer.decorate('d0')
63 | def a():
64 | pass
65 | a()
66 |
67 | assert self.get_time(mock_client, 'timer.eggs0.d0') == 123.4, \
68 | 'This test must execute within 2ms'
69 |
70 |
71 | class TestTimerContextManager(TestTimerBase):
72 |
73 | def setUp(self):
74 | self.timer = statsd.Timer('cm')
75 |
76 | # get time.time() to always return the same value so that this test
77 | # isn't system load dependant.
78 | self._time_patch = mock.patch('time.time')
79 | time_time = self._time_patch.start()
80 |
81 | def generator():
82 | i = 0.0
83 | while True:
84 | i += 0.1234
85 | yield i
86 | time_time.side_effect = generator()
87 |
88 | @mock.patch('statsd.Client')
89 | def test_context_manager(self, mock_client):
90 | timer = statsd.Timer('cm')
91 | with timer:
92 | # Do something here
93 | pass
94 |
95 | assert self.get_time(mock_client, 'cm.total') == 123.4, \
96 | 'This test must execute within 2ms'
97 |
98 | @mock.patch('statsd.Client')
99 | def test_context_manager_default(self, mock_client):
100 | timer = self.timer.get_client('default')
101 | with timer.time():
102 | pass
103 |
104 | assert self.get_time(mock_client, 'cm.default') == 123.4, \
105 | 'This test must execute within 2ms'
106 |
107 | @mock.patch('statsd.Client')
108 | def test_context_manager_named(self, mock_client):
109 | timer = self.timer.get_client('named')
110 | with timer.time('name'):
111 | pass
112 |
113 | assert self.get_time(mock_client, 'cm.named.name') == 123.4, \
114 | 'This test must execute within 2ms'
115 |
116 | @mock.patch('statsd.Client')
117 | def test_context_manager_class(self, mock_client):
118 | timer = self.timer.get_client('named')
119 | with timer.time(class_=statsd.Timer):
120 | pass
121 |
122 | assert self.get_time(mock_client, 'cm.named') == 123.4, \
123 | 'This test must execute within 2ms'
124 |
125 |
126 | class TestTimerAdvancedUsage(TestTimerDecorator):
127 |
128 | @mock.patch('statsd.Client')
129 | def test_timer_total(self, mock_client):
130 | timer4 = statsd.Timer('timer4')
131 | timer4.start()
132 | timer4.stop()
133 | assert self.get_time(mock_client, 'timer4.total') == 123.4, \
134 | 'This test must execute within 2ms'
135 |
136 | timer5 = statsd.Timer('timer5')
137 | timer5.start()
138 | timer5.stop('test')
139 | assert self.get_time(mock_client, 'timer5.test') == 123.4, \
140 | 'This test must execute within 2ms'
141 |
142 | @mock.patch('statsd.Client')
143 | def test_timer_intermediate(self, mock_client):
144 | timer6 = statsd.Timer('timer6')
145 | timer6.start()
146 | timer6.intermediate('extras')
147 | assert self.get_time(mock_client, 'timer6.extras') == 123.4, \
148 | 'This test must execute within 2ms'
149 | timer6.stop()
150 | assert self.get_time(mock_client, 'timer6.total') == 370.2, \
151 | 'This test must execute within 2ms'
152 |
153 | timer7 = statsd.Timer('timer7')
154 | timer7.start()
155 | timer7.intermediate('extras')
156 | assert self.get_time(mock_client, 'timer7.extras') == 123.4, \
157 | 'This test must execute within 2ms'
158 | timer7.stop('test')
159 | assert self.get_time(mock_client, 'timer7.test') == 370.2, \
160 | 'This test must execute within 2ms'
161 |
162 |
163 | class TestTimerZero(TestTimerBase):
164 |
165 | def setUp(self):
166 | # get time.time() to always return the same value so that this test
167 | # isn't system load dependant.
168 | self._time_patch = mock.patch('time.time')
169 | time_time = self._time_patch.start()
170 |
171 | def generator():
172 | while True:
173 | yield 0
174 | time_time.side_effect = generator()
175 |
176 | def tearDown(self):
177 | self._time_patch.stop()
178 |
179 | @mock.patch('statsd.Client')
180 | def test_timer_zero(self, mock_client):
181 | timer8 = statsd.Timer('timer8', min_send_threshold=0)
182 | timer8.start()
183 | timer8.stop()
184 | assert mock_client._send.call_args is None, \
185 | '0 timings shouldnt be sent'
186 |
187 | timer9 = statsd.Timer('timer9', min_send_threshold=0)
188 | timer9.start()
189 | timer9.stop('test')
190 | assert mock_client._send.call_args is None, \
191 | '0 timings shouldnt be sent'
192 |
193 |
--------------------------------------------------------------------------------
/docs/_theme/wolph/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz.
6 | * :license: Flask Design License, see LICENSE for details.
7 | */
8 |
9 | {% set page_width = '940px' %}
10 | {% set sidebar_width = '220px' %}
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro';
18 | font-size: 17px;
19 | background-color: white;
20 | color: #000;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.document {
26 | width: {{ page_width }};
27 | margin: 30px auto 0 auto;
28 | }
29 |
30 | div.documentwrapper {
31 | float: left;
32 | width: 100%;
33 | }
34 |
35 | div.bodywrapper {
36 | margin: 0 0 0 {{ sidebar_width }};
37 | }
38 |
39 | div.sphinxsidebar {
40 | width: {{ sidebar_width }};
41 | }
42 |
43 | hr {
44 | border: 1px solid #B1B4B6;
45 | }
46 |
47 | div.body {
48 | background-color: #ffffff;
49 | color: #3E4349;
50 | padding: 0 30px 0 30px;
51 | }
52 |
53 | img.floatingflask {
54 | padding: 0 0 10px 10px;
55 | float: right;
56 | }
57 |
58 | div.footer {
59 | width: {{ page_width }};
60 | margin: 20px auto 30px auto;
61 | font-size: 14px;
62 | color: #888;
63 | text-align: right;
64 | }
65 |
66 | div.footer a {
67 | color: #888;
68 | }
69 |
70 | div.related {
71 | display: none;
72 | }
73 |
74 | div.sphinxsidebar a {
75 | color: #444;
76 | text-decoration: none;
77 | border-bottom: 1px dotted #999;
78 | }
79 |
80 | div.sphinxsidebar a:hover {
81 | border-bottom: 1px solid #999;
82 | }
83 |
84 | div.sphinxsidebar {
85 | font-size: 14px;
86 | line-height: 1.5;
87 | }
88 |
89 | div.sphinxsidebarwrapper {
90 | padding: 0px 10px;
91 | }
92 |
93 | div.sphinxsidebarwrapper p.logo {
94 | padding: 0 0 20px 0;
95 | margin: 0;
96 | text-align: center;
97 | }
98 |
99 | div.sphinxsidebar h3,
100 | div.sphinxsidebar h4 {
101 | font-family: 'Garamond', 'Georgia', serif;
102 | color: #555;
103 | font-size: 24px;
104 | font-weight: normal;
105 | margin: 0 0 5px 0;
106 | padding: 0;
107 | }
108 |
109 | div.sphinxsidebar h4 {
110 | font-size: 20px;
111 | }
112 |
113 | div.sphinxsidebar h3 a {
114 | color: #444;
115 | }
116 |
117 | div.sphinxsidebar p.logo a,
118 | div.sphinxsidebar h3 a,
119 | div.sphinxsidebar p.logo a:hover,
120 | div.sphinxsidebar h3 a:hover {
121 | border: none;
122 | }
123 |
124 | div.sphinxsidebar p {
125 | color: #555;
126 | margin: 10px 0;
127 | }
128 |
129 | div.sphinxsidebar ul {
130 | margin: 10px 0;
131 | padding: 0;
132 | color: #000;
133 | }
134 |
135 | div.sphinxsidebar input[type="text"] {
136 | width: 160px!important;
137 | }
138 | div.sphinxsidebar input {
139 | border: 1px solid #ccc;
140 | font-family: 'Georgia', serif;
141 | font-size: 1em;
142 | }
143 |
144 | /* -- body styles ----------------------------------------------------------- */
145 |
146 | a {
147 | color: #004B6B;
148 | text-decoration: underline;
149 | }
150 |
151 | a:hover {
152 | color: #6D4100;
153 | text-decoration: underline;
154 | }
155 |
156 | div.body h1,
157 | div.body h2,
158 | div.body h3,
159 | div.body h4,
160 | div.body h5,
161 | div.body h6 {
162 | font-family: 'Garamond', 'Georgia', serif;
163 | font-weight: normal;
164 | margin: 30px 0px 10px 0px;
165 | padding: 0;
166 | }
167 |
168 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
169 | div.body h2 { font-size: 180%; }
170 | div.body h3 { font-size: 150%; }
171 | div.body h4 { font-size: 130%; }
172 | div.body h5 { font-size: 100%; }
173 | div.body h6 { font-size: 100%; }
174 |
175 | a.headerlink {
176 | color: #ddd;
177 | padding: 0 4px;
178 | text-decoration: none;
179 | }
180 |
181 | a.headerlink:hover {
182 | color: #444;
183 | background: #eaeaea;
184 | }
185 |
186 | div.body p, div.body dd, div.body li {
187 | line-height: 1.4em;
188 | }
189 |
190 | div.admonition {
191 | background: #fafafa;
192 | margin: 20px -30px;
193 | padding: 10px 30px;
194 | border-top: 1px solid #ccc;
195 | border-bottom: 1px solid #ccc;
196 | }
197 |
198 | div.admonition tt.xref, div.admonition a tt {
199 | border-bottom: 1px solid #fafafa;
200 | }
201 |
202 | dd div.admonition {
203 | margin-left: -60px;
204 | padding-left: 60px;
205 | }
206 |
207 | div.admonition p.admonition-title {
208 | font-family: 'Garamond', 'Georgia', serif;
209 | font-weight: normal;
210 | font-size: 24px;
211 | margin: 0 0 10px 0;
212 | padding: 0;
213 | line-height: 1;
214 | }
215 |
216 | div.admonition p.last {
217 | margin-bottom: 0;
218 | }
219 |
220 | div.highlight {
221 | background-color: white;
222 | }
223 |
224 | dt:target, .highlight {
225 | background: #FAF3E8;
226 | }
227 |
228 | div.note {
229 | background-color: #eee;
230 | border: 1px solid #ccc;
231 | }
232 |
233 | div.seealso {
234 | background-color: #ffc;
235 | border: 1px solid #ff6;
236 | }
237 |
238 | div.topic {
239 | background-color: #eee;
240 | }
241 |
242 | p.admonition-title {
243 | display: inline;
244 | }
245 |
246 | p.admonition-title:after {
247 | content: ":";
248 | }
249 |
250 | pre, tt {
251 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
252 | font-size: 0.9em;
253 | }
254 |
255 | img.screenshot {
256 | }
257 |
258 | tt.descname, tt.descclassname {
259 | font-size: 0.95em;
260 | }
261 |
262 | tt.descname {
263 | padding-right: 0.08em;
264 | }
265 |
266 | img.screenshot {
267 | -moz-box-shadow: 2px 2px 4px #eee;
268 | -webkit-box-shadow: 2px 2px 4px #eee;
269 | box-shadow: 2px 2px 4px #eee;
270 | }
271 |
272 | table.docutils {
273 | border: 1px solid #888;
274 | -moz-box-shadow: 2px 2px 4px #eee;
275 | -webkit-box-shadow: 2px 2px 4px #eee;
276 | box-shadow: 2px 2px 4px #eee;
277 | }
278 |
279 | table.docutils td, table.docutils th {
280 | border: 1px solid #888;
281 | padding: 0.25em 0.7em;
282 | }
283 |
284 | table.field-list, table.footnote {
285 | border: none;
286 | -moz-box-shadow: none;
287 | -webkit-box-shadow: none;
288 | box-shadow: none;
289 | }
290 |
291 | table.footnote {
292 | margin: 15px 0;
293 | width: 100%;
294 | border: 1px solid #eee;
295 | background: #fdfdfd;
296 | font-size: 0.9em;
297 | }
298 |
299 | table.footnote + table.footnote {
300 | margin-top: -15px;
301 | border-top: none;
302 | }
303 |
304 | table.field-list th {
305 | padding: 0 0.8em 0 0;
306 | }
307 |
308 | table.field-list td {
309 | padding: 0;
310 | }
311 |
312 | table.footnote td.label {
313 | width: 0px;
314 | padding: 0.3em 0 0.3em 0.5em;
315 | }
316 |
317 | table.footnote td {
318 | padding: 0.3em 0.5em;
319 | }
320 |
321 | dl {
322 | margin: 0;
323 | padding: 0;
324 | }
325 |
326 | dl dd {
327 | margin-left: 30px;
328 | }
329 |
330 | blockquote {
331 | margin: 0 0 0 30px;
332 | padding: 0;
333 | }
334 |
335 | ul, ol {
336 | margin: 10px 0 10px 30px;
337 | padding: 0;
338 | }
339 |
340 | pre {
341 | background: #eee;
342 | padding: 7px 30px;
343 | margin: 15px -30px;
344 | line-height: 1.3em;
345 | }
346 |
347 | dl pre, blockquote pre, li pre {
348 | margin-left: -60px;
349 | padding-left: 60px;
350 | }
351 |
352 | dl dl pre {
353 | margin-left: -90px;
354 | padding-left: 90px;
355 | }
356 |
357 | tt {
358 | background-color: #ecf0f3;
359 | color: #222;
360 | /* padding: 1px 2px; */
361 | }
362 |
363 | tt.xref, a tt {
364 | background-color: #FBFBFB;
365 | border-bottom: 1px solid white;
366 | }
367 |
368 | a.reference {
369 | text-decoration: none;
370 | border-bottom: 1px dotted #004B6B;
371 | }
372 |
373 | a.reference:hover {
374 | border-bottom: 1px solid #6D4100;
375 | }
376 |
377 | a.footnote-reference {
378 | text-decoration: none;
379 | font-size: 0.7em;
380 | vertical-align: top;
381 | border-bottom: 1px dotted #004B6B;
382 | }
383 |
384 | a.footnote-reference:hover {
385 | border-bottom: 1px solid #6D4100;
386 | }
387 |
388 | a:hover tt {
389 | background: #EEE;
390 | }
391 |
392 |
393 | /* scrollbars */
394 |
395 | ::-webkit-scrollbar {
396 | width: 6px;
397 | height: 6px;
398 | }
399 |
400 | ::-webkit-scrollbar-button:start:decrement,
401 | ::-webkit-scrollbar-button:end:increment {
402 | display: block;
403 | height: 10px;
404 | }
405 |
406 | ::-webkit-scrollbar-button:vertical:increment {
407 | background-color: #fff;
408 | }
409 |
410 | ::-webkit-scrollbar-track-piece {
411 | background-color: #eee;
412 | -webkit-border-radius: 3px;
413 | }
414 |
415 | ::-webkit-scrollbar-thumb:vertical {
416 | height: 50px;
417 | background-color: #ccc;
418 | -webkit-border-radius: 3px;
419 | }
420 |
421 | ::-webkit-scrollbar-thumb:horizontal {
422 | width: 50px;
423 | background-color: #ccc;
424 | -webkit-border-radius: 3px;
425 | }
426 |
427 | /* misc. */
428 |
429 | .revsys-inline {
430 | display: none!important;
431 | }
432 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Python Statsd documentation build configuration file, created by
4 | # sphinx-quickstart on Mon May 23 12:28:27 2011.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import os
15 | import sys
16 | import datetime
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | sys.path.insert(0, os.path.abspath(os.path.pardir))
22 |
23 | from statsd import __about__ as statsd
24 |
25 | # -- General configuration -----------------------------------------------------
26 |
27 | # Add any Sphinx extension module names here, as strings. They can be extensions
28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
29 | extensions = [
30 | 'sphinx.ext.autodoc',
31 | 'sphinx.ext.doctest',
32 | 'sphinx.ext.intersphinx',
33 | 'sphinx.ext.todo',
34 | 'sphinx.ext.coverage',
35 | 'sphinx.ext.viewcode',
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # The suffix of source filenames.
42 | source_suffix = '.rst'
43 |
44 | # The encoding of source files.
45 | #source_encoding = 'utf-8'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 |
50 | # General information about the project.
51 | project = statsd.__package_name__.capitalize()
52 | copyright = '%s, %s' % (
53 | datetime.date.today().year,
54 | statsd.__author__,
55 | )
56 |
57 | # The version info for the project you're documenting, acts as replacement for
58 | # |version| and |release|, also used in various other places throughout the
59 | # built documents.
60 | #
61 | # The short X.Y version.
62 | version = statsd.__version__
63 | # The full version, including alpha/beta/rc tags.
64 | release = statsd.__version__
65 |
66 | # Monkey patch to disable nonlocal image warning
67 | import sphinx
68 | original_warn_mode = sphinx.environment.BuildEnvironment.warn_node
69 |
70 |
71 | def allow_nonlocal_image_warn_node(self, msg, node):
72 | if not msg.startswith('nonlocal image URI found:'):
73 | original_warn_mode(self, msg, node)
74 |
75 | sphinx.environment.BuildEnvironment.warn_node = allow_nonlocal_image_warn_node
76 |
77 | suppress_warnings = [
78 | 'image.nonlocal_uri',
79 | ]
80 |
81 | needs_sphinx = '1.4'
82 |
83 | # The language for content autogenerated by Sphinx. Refer to documentation
84 | # for a list of supported languages.
85 | #language = None
86 |
87 | # There are two options for replacing |today|: either, you set today to some
88 | # non-false value, then it is used:
89 | #today = ''
90 | # Else, today_fmt is used as the format for a strftime call.
91 | #today_fmt = '%B %d, %Y'
92 |
93 | # List of documents that shouldn't be included in the build.
94 | #unused_docs = []
95 |
96 | # List of directories, relative to source directory, that shouldn't be searched
97 | # for source files.
98 | exclude_trees = []
99 |
100 | # The reST default role (used for this markup: `text`) to use for all documents.
101 | #default_role = None
102 |
103 | # If true, '()' will be appended to :func: etc. cross-reference text.
104 | #add_function_parentheses = True
105 |
106 | # If true, the current module name will be prepended to all description
107 | # unit titles (such as .. function::).
108 | #add_module_names = True
109 |
110 | # If true, sectionauthor and moduleauthor directives will be shown in the
111 | # output. They are ignored by default.
112 | #show_authors = False
113 |
114 | # The name of the Pygments (syntax highlighting) style to use.
115 | pygments_style = 'sphinx'
116 |
117 | # A list of ignored prefixes for module index sorting.
118 | #modindex_common_prefix = []
119 |
120 |
121 | # -- Options for HTML output ---------------------------------------------------
122 |
123 | # The theme to use for HTML and HTML Help pages. Major themes that come with
124 | # Sphinx are currently 'default' and 'sphinxdoc'.
125 | html_theme = 'wolph'
126 |
127 | # Theme options are theme-specific and customize the look and feel of a theme
128 | # further. For a list of options available for each theme, see the
129 | # documentation.
130 | #html_theme_options = {}
131 |
132 | # Add any paths that contain custom themes here, relative to this directory.
133 | html_theme_path = ['_theme']
134 |
135 | # The name for this set of Sphinx documents. If None, it defaults to
136 | # " v documentation".
137 | #html_title = None
138 |
139 | # A shorter title for the navigation bar. Default is the same as html_title.
140 | #html_short_title = None
141 |
142 | # The name of an image file (relative to this directory) to place at the top
143 | # of the sidebar.
144 | #html_logo = None
145 |
146 | # The name of an image file (within the static path) to use as favicon of the
147 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
148 | # pixels large.
149 | #html_favicon = None
150 |
151 | # Add any paths that contain custom static files (such as style sheets) here,
152 | # relative to this directory. They are copied after the builtin static files,
153 | # so a file named "default.css" will overwrite the builtin "default.css".
154 | #html_static_path = ['_static']
155 |
156 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
157 | # using the given strftime format.
158 | #html_last_updated_fmt = '%b %d, %Y'
159 |
160 | # If true, SmartyPants will be used to convert quotes and dashes to
161 | # typographically correct entities.
162 | #html_use_smartypants = True
163 |
164 | # Custom sidebar templates, maps document names to template names.
165 | #html_sidebars = {}
166 |
167 | # Additional templates that should be rendered to pages, maps page names to
168 | # template names.
169 | #html_additional_pages = {}
170 |
171 | # If false, no module index is generated.
172 | #html_use_modindex = True
173 |
174 | # If false, no index is generated.
175 | #html_use_index = True
176 |
177 | # If true, the index is split into individual pages for each letter.
178 | #html_split_index = False
179 |
180 | # If true, links to the reST sources are added to the pages.
181 | #html_show_sourcelink = True
182 |
183 | # If true, an OpenSearch description file will be output, and all pages will
184 | # contain a tag referring to it. The value of this option must be the
185 | # base URL from which the finished HTML is served.
186 | #html_use_opensearch = ''
187 |
188 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
189 | #html_file_suffix = ''
190 |
191 | # Output file base name for HTML help builder.
192 | htmlhelp_basename = 'PythonStatsddoc'
193 |
194 |
195 | # -- Options for LaTeX output --------------------------------------------------
196 |
197 | # The paper size ('letter' or 'a4').
198 | #latex_paper_size = 'letter'
199 |
200 | # The font size ('10pt', '11pt' or '12pt').
201 | #latex_font_size = '10pt'
202 |
203 | # Grouping the document tree into LaTeX files. List of tuples
204 | # (source start file, target name, title, author, documentclass [howto/manual]).
205 | latex_documents = [
206 | ('index', 'PythonStatsd.tex', u'Python Statsd Documentation',
207 | statsd.__author__, 'manual'),
208 | ]
209 |
210 | # The name of an image file (relative to this directory) to place at the top of
211 | # the title page.
212 | #latex_logo = None
213 |
214 | # For "manual" documents, if this is true, then toplevel headings are parts,
215 | # not chapters.
216 | #latex_use_parts = False
217 |
218 | # Additional stuff for the LaTeX preamble.
219 | #latex_preamble = ''
220 |
221 | # Documents to append as an appendix to all manuals.
222 | #latex_appendices = []
223 |
224 | # If false, no module index is generated.
225 | #latex_domain_indices = True
226 |
227 |
228 | # -- Options for manual page output --------------------------------------------
229 |
230 | # One entry per manual page. List of tuples
231 | # (source start file, name, description, authors, manual section).
232 | man_pages = [
233 | ('index', 'statsd', u'Python Statsd Documentation',
234 | [statsd.__author__], 1)
235 | ]
236 |
237 | # If true, show URL addresses after external links.
238 | #man_show_urls = False
239 |
240 |
241 | # -- Options for Texinfo output ------------------------------------------------
242 |
243 | # Grouping the document tree into Texinfo files. List of tuples
244 | # (source start file, target name, title, author,
245 | # dir menu entry, description, category)
246 | texinfo_documents = [
247 | ('index', 'PythonStatsd', u'Python Statsd Documentation',
248 | statsd.__author__, 'PythonStatsd', statsd.__description__,
249 | 'Miscellaneous'),
250 | ]
251 |
252 | # Documents to append as an appendix to all manuals.
253 | #texinfo_appendices = []
254 |
255 | # If false, no module index is generated.
256 | #texinfo_domain_indices = True
257 |
258 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
259 | #texinfo_show_urls = 'footnote'
260 |
261 |
262 | # -- Options for Epub output ---------------------------------------------------
263 |
264 | # Bibliographic Dublin Core info.
265 | epub_title = u'Python Statsd'
266 | epub_author = statsd.__author__
267 | epub_publisher = statsd.__author__
268 | epub_copyright = copyright
269 |
270 | # The language of the text. It defaults to the language option
271 | # or en if the language is not set.
272 | #epub_language = ''
273 |
274 | # The scheme of the identifier. Typical schemes are ISBN or URL.
275 | #epub_scheme = ''
276 |
277 | # The unique identifier of the text. This can be a ISBN number
278 | # or the project homepage.
279 | #epub_identifier = ''
280 |
281 | # A unique identification for the text.
282 | #epub_uid = ''
283 |
284 | # A tuple containing the cover image and cover page html template filenames.
285 | #epub_cover = ()
286 |
287 | # HTML files that should be inserted before the pages created by sphinx.
288 | # The format is a list of tuples containing the path and title.
289 | #epub_pre_files = []
290 |
291 | # HTML files shat should be inserted after the pages created by sphinx.
292 | # The format is a list of tuples containing the path and title.
293 | #epub_post_files = []
294 |
295 | # A list of files that should not be packed into the epub file.
296 | #epub_exclude_files = []
297 |
298 | # The depth of the table of contents in toc.ncx.
299 | #epub_tocdepth = 3
300 |
301 | # Allow duplicate toc entries.
302 | #epub_tocdup = True
303 |
304 |
305 | # Example configuration for intersphinx: refer to the Python standard library.
306 | intersphinx_mapping = {'http://docs.python.org/': None}
307 |
--------------------------------------------------------------------------------