././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605520730.439949 aiodogstatsd-0.14.0/LICENSE0000644000000000000000000000205700000000000012161 0ustar00MIT License Copyright (c) 2019 Nikita Grishko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605520730.439949 aiodogstatsd-0.14.0/README.md0000644000000000000000000000464100000000000012434 0ustar00# aiodogstatsd [![Build Status](https://github.com/Gr1N/aiodogstatsd/workflows/default/badge.svg)](https://github.com/Gr1N/aiodogstatsd/actions?query=workflow%3Adefault) [![codecov](https://codecov.io/gh/Gr1N/aiodogstatsd/branch/master/graph/badge.svg)](https://codecov.io/gh/Gr1N/aiodogstatsd) ![PyPI](https://img.shields.io/pypi/v/aiodogstatsd.svg?label=pypi%20version) ![PyPI - Downloads](https://img.shields.io/pypi/dm/aiodogstatsd.svg?label=pypi%20downloads) ![GitHub](https://img.shields.io/github/license/Gr1N/aiodogstatsd.svg) An asyncio-based client for sending metrics to StatsD with support of [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/) extension. Library fully tested with [statsd_exporter](https://github.com/prometheus/statsd_exporter) and supports `gauge`, `counter`, `histogram`, `distribution` and `timing` types. `aiodogstatsd` client by default uses _9125_ port. It's a default port for [statsd_exporter](https://github.com/prometheus/statsd_exporter) and it's different from _8125_ which is used by default in StatsD and [DataDog](https://www.datadoghq.com/). Initialize the client with the proper port you need if it's different from _9125_. ## Installation Just type: ```sh $ pip install aiodogstatsd ``` ## At a glance Just simply use client as a context manager and send any metric you want: ```python import asyncio import aiodogstatsd async def main(): async with aiodogstatsd.Client() as client: client.increment("users.online") asyncio.run(main()) ``` Please follow [documentation](https://gr1n.github.io/aiodogstatsd) or look at [`examples/`](https://github.com/Gr1N/aiodogstatsd/tree/master/examples) directory to find more examples of library usage, e.g. integration with [`AIOHTTP`](https://aiohttp.readthedocs.io/), [`Sanic`](https://sanicframework.org/) or [`Starlette`](https://www.starlette.io) frameworks. ## Contributing To work on the `aiodogstatsd` codebase, you'll want to clone the project locally and install the required dependencies via [poetry](https://poetry.eustace.io): ```sh $ git clone git@github.com:Gr1N/aiodogstatsd.git $ make install ``` To run tests and linters use command below: ```sh $ make lint && make test ``` If you want to run only tests or linters you can explicitly specify which test environment you want to run, e.g.: ```sh $ make lint-black ``` ## License `aiodogstatsd` is licensed under the MIT license. See the license file for details. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605520730.439949 aiodogstatsd-0.14.0/aiodogstatsd/__init__.py0000644000000000000000000000006200000000000015744 0ustar00from .client import Client __all__ = ("Client",) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/client.py0000644000000000000000000002155500000000000015475 0ustar00import asyncio from asyncio.transports import DatagramTransport from contextlib import contextmanager from random import random from typing import Iterator, Optional from aiodogstatsd import protocol, typedefs from aiodogstatsd.compat import get_event_loop __all__ = ("Client",) class Client: __slots__ = ( "_host", "_port", "_namespace", "_constant_tags", "_state", "_protocol", "_pending_queue", "_pending_queue_size", "_listen_future", "_listen_future_join", "_read_timeout", "_close_timeout", "_sample_rate", ) @property def connected(self) -> bool: return self._state == typedefs.CState.CONNECTED @property def closing(self) -> bool: return self._state == typedefs.CState.CLOSING @property def disconnected(self) -> bool: return self._state == typedefs.CState.DISCONNECTED def __init__( self, *, host: str = "localhost", port: int = 9125, namespace: Optional[typedefs.MNamespace] = None, constant_tags: Optional[typedefs.MTags] = None, read_timeout: float = 0.5, close_timeout: Optional[float] = None, sample_rate: typedefs.MSampleRate = 1, pending_queue_size: int = 2 ** 16, ) -> None: """ Initialize a client object. You can pass `host` and `port` of the DogStatsD server, `namespace` to prefix all metric names, `constant_tags` to attach to all metrics. Also, you can specify: `read_timeout` which will be used to read messages from an AsyncIO queue; `close_timeout` which will be used as wait time for client closing; `sample_rate` can be used for adjusting the frequency of stats sending. """ self._host = host self._port = port self._namespace = namespace self._constant_tags = constant_tags or {} self._state = typedefs.CState.DISCONNECTED self._protocol = DatagramProtocol() self._pending_queue: asyncio.Queue self._pending_queue_size = pending_queue_size self._listen_future: asyncio.Future self._listen_future_join: asyncio.Future self._read_timeout = read_timeout self._close_timeout = close_timeout self._sample_rate = sample_rate async def __aenter__(self) -> "Client": await self.connect() return self async def __aexit__(self, *args) -> None: await self.close() async def connect(self) -> None: loop = get_event_loop() await loop.create_datagram_endpoint( lambda: self._protocol, remote_addr=(self._host, self._port) ) self._pending_queue = asyncio.Queue(maxsize=self._pending_queue_size) self._listen_future = asyncio.ensure_future(self._listen()) self._listen_future_join = asyncio.Future() self._state = typedefs.CState.CONNECTED async def close(self) -> None: self._state = typedefs.CState.CLOSING try: await asyncio.wait_for(self._close(), timeout=self._close_timeout) except asyncio.TimeoutError: pass self._state = typedefs.CState.DISCONNECTED async def _close(self) -> None: await self._listen_future_join self._listen_future.cancel() await self._protocol.close() def gauge( self, name: typedefs.MName, *, value: typedefs.MValue, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Record the value of a gauge, optionally setting tags and a sample rate. """ self._report(name, typedefs.MType.GAUGE, value, tags, sample_rate) def increment( self, name: typedefs.MName, *, value: typedefs.MValue = 1, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Increment a counter, optionally setting a value, tags and a sample rate. """ self._report(name, typedefs.MType.COUNTER, value, tags, sample_rate) def decrement( self, name: typedefs.MName, *, value: typedefs.MValue = 1, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Decrement a counter, optionally setting a value, tags and a sample rate. """ value = -value if value else value self._report(name, typedefs.MType.COUNTER, value, tags, sample_rate) def histogram( self, name: typedefs.MName, *, value: typedefs.MValue, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Sample a histogram value, optionally setting tags and a sample rate. """ self._report(name, typedefs.MType.HISTOGRAM, value, tags, sample_rate) def distribution( self, name: typedefs.MName, *, value: typedefs.MValue, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Send a global distribution value, optionally setting tags and a sample rate. """ self._report(name, typedefs.MType.DISTRIBUTION, value, tags, sample_rate) def timing( self, name: typedefs.MName, *, value: typedefs.MValue, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: """ Record a timing, optionally setting tags and a sample rate. """ self._report(name, typedefs.MType.TIMING, value, tags, sample_rate) async def _listen(self) -> None: try: while self.connected: await self._listen_and_send() finally: # Note that `asyncio.CancelledError` raised on app clean up # Try to send remaining enqueued metrics if any while not self._pending_queue.empty(): await self._listen_and_send() self._listen_future_join.set_result(True) async def _listen_and_send(self) -> None: coro = self._pending_queue.get() try: buf = await asyncio.wait_for(coro, timeout=self._read_timeout) except asyncio.TimeoutError: pass else: self._protocol.send(buf) def _report( self, name: typedefs.MName, type_: typedefs.MType, value: typedefs.MValue, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> None: # Ignore any new incoming metric if client in closing or disconnected state if self.closing or self.disconnected: return sample_rate = sample_rate or self._sample_rate if sample_rate != 1 and random() > sample_rate: return # Resolve full tags list all_tags = dict(self._constant_tags, **tags or {}) # Build metric metric = protocol.build( name=name, namespace=self._namespace, value=value, type_=type_, tags=all_tags, sample_rate=sample_rate, ) # Enqueue metric try: self._pending_queue.put_nowait(metric) except asyncio.QueueFull: pass @contextmanager def timeit( self, name: typedefs.MName, *, tags: Optional[typedefs.MTags] = None, sample_rate: Optional[typedefs.MSampleRate] = None, ) -> Iterator[None]: """ Context manager for easily timing methods. """ loop = get_event_loop() started_at = loop.time() try: yield finally: value = (loop.time() - started_at) * 1000 self.timing(name, value=int(value), tags=tags, sample_rate=sample_rate) class DatagramProtocol(asyncio.DatagramProtocol): __slots__ = ("_transport", "_closed") def __init__(self) -> None: self._transport: Optional[DatagramTransport] = None self._closed: asyncio.Future async def close(self) -> None: if self._transport is None: return self._transport.close() await self._closed def connection_made(self, transport): self._transport = transport self._closed = asyncio.Future() def connection_lost(self, _exc): self._transport = None self._closed.set_result(True) def send(self, data: bytes) -> None: if self._transport is None: return try: self._transport.sendto(data) except Exception: # Errors should fail silently so they don't affect anything else pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/compat.py0000644000000000000000000000043100000000000015470 0ustar00import asyncio import sys __all__ = ("get_event_loop",) def _get_event_loop_factory(): # pragma: no cover if sys.version_info >= (3, 7): return asyncio.get_running_loop # type: ignore return asyncio.get_event_loop get_event_loop = _get_event_loop_factory() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/contrib/__init__.py0000644000000000000000000000000000000000000017374 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/contrib/aiohttp.py0000644000000000000000000001006000000000000017314 0ustar00from http import HTTPStatus from typing import AsyncIterator, Callable, Optional, cast from aiohttp import web from aiohttp.web_app import _Middleware from aiohttp.web_routedef import _SimpleHandler from aiohttp.web_urldispatcher import DynamicResource, MatchInfoError from aiodogstatsd import Client, typedefs from aiodogstatsd.compat import get_event_loop __all__ = ( "DEFAULT_CLIENT_APP_KEY", "DEAFULT_REQUEST_DURATION_METRIC_NAME", "cleanup_context_factory", "middleware_factory", ) DEFAULT_CLIENT_APP_KEY = "statsd" DEAFULT_REQUEST_DURATION_METRIC_NAME = "http_request_duration" def cleanup_context_factory( *, client_app_key: str = DEFAULT_CLIENT_APP_KEY, host: str = "localhost", port: int = 9125, namespace: Optional[typedefs.MNamespace] = None, constant_tags: Optional[typedefs.MTags] = None, read_timeout: float = 0.5, close_timeout: Optional[float] = None, sample_rate: typedefs.MSampleRate = 1, ) -> Callable[[web.Application], AsyncIterator[None]]: async def cleanup_context(app: web.Application) -> AsyncIterator[None]: app[client_app_key] = Client( host=host, port=port, namespace=namespace, constant_tags=constant_tags, read_timeout=read_timeout, close_timeout=close_timeout, sample_rate=sample_rate, ) await app[client_app_key].connect() yield await app[client_app_key].close() return cleanup_context def middleware_factory( *, client_app_key: str = DEFAULT_CLIENT_APP_KEY, request_duration_metric_name: str = DEAFULT_REQUEST_DURATION_METRIC_NAME, collect_not_allowed: bool = False, collect_not_found: bool = False, ) -> _Middleware: @web.middleware async def middleware( request: web.Request, handler: _SimpleHandler ) -> web.StreamResponse: loop = get_event_loop() request_started_at = loop.time() # By default response status is 500 because we don't want to write any logic for # catching exceptions except exceptions which inherited from # `web.HTTPException`. And also we will override response status in case of any # successful handler execution. response_status = cast(int, HTTPStatus.INTERNAL_SERVER_ERROR.value) try: response = await handler(request) response_status = response.status except web.HTTPException as e: response_status = e.status raise e finally: if _proceed_collecting( # pragma: no branch request, response_status, collect_not_allowed, collect_not_found ): request_duration = (loop.time() - request_started_at) * 1000 request.app[client_app_key].timing( # pragma: no branch request_duration_metric_name, value=request_duration, tags={ "method": request.method, "path": _derive_request_path(request), "status": response_status, }, ) return response return middleware def _proceed_collecting( request: web.Request, response_status: int, collect_not_allowed: bool, collect_not_found: bool, ) -> bool: if isinstance(request.match_info, MatchInfoError) and ( (response_status == HTTPStatus.METHOD_NOT_ALLOWED and not collect_not_allowed) or (response_status == HTTPStatus.NOT_FOUND and not collect_not_found) ): return False return True def _derive_request_path(request: web.Request) -> str: # AIOHTTP has a lot of different route resources like DynamicResource and we need to # process them correctly to get a valid original request path, so if you found an # issue with the request path in your metrics then you need to go here and extend # deriving logic. if isinstance(request.match_info.route.resource, DynamicResource): return request.match_info.route.resource.canonical return request.path ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/contrib/sanic.py0000644000000000000000000000660000000000000016746 0ustar00from asyncio import AbstractEventLoop from http import HTTPStatus from typing import Awaitable, Callable, Optional, Tuple from sanic import Sanic from sanic.exceptions import MethodNotSupported, NotFound from sanic.request import Request from sanic.response import HTTPResponse from aiodogstatsd import Client, typedefs from aiodogstatsd.compat import get_event_loop __all__ = ( "DEFAULT_CLIENT_APP_KEY", "DEAFULT_REQUEST_DURATION_METRIC_NAME", "listeners_factory", "middlewares_factory", ) DEFAULT_CLIENT_APP_KEY = "statsd" DEAFULT_REQUEST_DURATION_METRIC_NAME = "http_request_duration" ListenerCallable = Callable[[Sanic, AbstractEventLoop], Awaitable] MiddlewareCallable = Callable[..., Awaitable] def listeners_factory( *, client_app_key: str = DEFAULT_CLIENT_APP_KEY, host: str = "localhost", port: int = 9125, namespace: Optional[typedefs.MNamespace] = None, constant_tags: Optional[typedefs.MTags] = None, read_timeout: float = 0.5, close_timeout: Optional[float] = None, sample_rate: typedefs.MSampleRate = 1, ) -> Tuple[ListenerCallable, ListenerCallable]: async def listener_setup(app: Sanic, loop: AbstractEventLoop) -> None: client = Client( host=host, port=port, namespace=namespace, constant_tags=constant_tags, read_timeout=read_timeout, close_timeout=close_timeout, sample_rate=sample_rate, ) await client.connect() setattr(app, client_app_key, client) async def listener_close(app: Sanic, loop: AbstractEventLoop) -> None: client = getattr(app, client_app_key) await client.close() return listener_setup, listener_close def middlewares_factory( *, client_app_key: str = DEFAULT_CLIENT_APP_KEY, request_duration_metric_name: str = DEAFULT_REQUEST_DURATION_METRIC_NAME, collect_not_allowed: bool = False, collect_not_found: bool = False, ) -> Tuple[MiddlewareCallable, MiddlewareCallable]: async def middleware_request(request: Request) -> None: request.ctx._statsd_request_started_at = get_event_loop().time() async def middleware_response(request: Request, response: HTTPResponse) -> None: if _proceed_collecting( request, response, collect_not_allowed, collect_not_found ): request_duration = ( get_event_loop().time() - request.ctx._statsd_request_started_at ) * 1000 getattr(request.app, client_app_key).timing( request_duration_metric_name, value=request_duration, tags={ "method": request.method, "path": request.path, "status": response.status, }, ) return middleware_request, middleware_response def _proceed_collecting( request: Request, response: HTTPResponse, collect_not_allowed: bool, collect_not_found: bool, ) -> bool: try: request.match_info except (MethodNotSupported, NotFound): request_match_info_error = True else: request_match_info_error = False if request_match_info_error and ( (response.status == HTTPStatus.METHOD_NOT_ALLOWED and not collect_not_allowed) or (response.status == HTTPStatus.NOT_FOUND and not collect_not_found) ): return False return True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/contrib/starlette.py0000644000000000000000000001015500000000000017660 0ustar00from http import HTTPStatus from typing import Awaitable, Callable, Optional, Tuple, cast from starlette.applications import Starlette from starlette.exceptions import HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from starlette.routing import Match as RouteMatch, Route, Router from aiodogstatsd import Client from aiodogstatsd.compat import get_event_loop __all__ = ( "DEAFULT_REQUEST_DURATION_METRIC_NAME", "StatsDMiddleware", ) DEAFULT_REQUEST_DURATION_METRIC_NAME = "http_request_duration" class StatsDMiddleware(BaseHTTPMiddleware): __slots__ = ( "_client", "_request_duration_metric_name", "_collect_not_allowed", "_collect_not_found", ) def __init__( self, app: Starlette, *, client: Client, request_duration_metric_name: str = DEAFULT_REQUEST_DURATION_METRIC_NAME, collect_not_allowed: bool = False, collect_not_found: bool = False, ) -> None: super().__init__(app) self._client = client self._request_duration_metric_name = request_duration_metric_name self._collect_not_allowed = collect_not_allowed self._collect_not_found = collect_not_found async def dispatch( self, request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: loop = get_event_loop() request_started_at = loop.time() # By default response status is 500 because we don't want to write any logic for # catching exceptions except exceptions which inherited from `HTTPException`. # And also we will override response status in case of any successful handler # execution. response_status = cast(int, HTTPStatus.INTERNAL_SERVER_ERROR.value) try: response = await call_next(request) response_status = response.status_code except HTTPException as e: # pragma: no cover # We kept exception handling here (just in case), but code looks useless. # We're unable to cover that part of code with tests because the framework # handles exceptions somehow different, somehow deeply inside. response_status = e.status_code raise e finally: request_path, request_path_template = _derive_request_path(request) if _proceed_collecting( # pragma: no branch request_path_template, response_status, self._collect_not_allowed, self._collect_not_found, ): request_duration = (loop.time() - request_started_at) * 1000 self._client.timing( # pragma: no branch self._request_duration_metric_name, value=request_duration, tags={ "method": request.method, "path": request_path_template or request_path, "status": response_status, }, ) return response def _proceed_collecting( request_path_template: Optional[str], response_status: int, collect_not_allowed: bool, collect_not_found: bool, ) -> bool: if ( request_path_template is None and response_status == HTTPStatus.NOT_FOUND and not collect_not_found ): return False elif response_status == HTTPStatus.METHOD_NOT_ALLOWED and not collect_not_allowed: return False return True def _derive_request_path(request: Request) -> Tuple[str, Optional[str]]: # We need somehow understand request path in templated view this needed in case of # parametrized routes. Current realization is not very efficient, but for now, there # is no better way to do such things. router: Router = request.scope["router"] for route in router.routes: match, _ = route.matches(request.scope) if match == RouteMatch.NONE: continue return request["path"], cast(Route, route).path return request["path"], None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/protocol.py0000644000000000000000000000137000000000000016051 0ustar00from typing import Optional from aiodogstatsd import typedefs __all__ = ("build", "build_tags") def build( *, name: typedefs.MName, namespace: Optional[typedefs.MNamespace], value: typedefs.MValue, type_: typedefs.MType, tags: typedefs.MTags, sample_rate: typedefs.MSampleRate, ) -> bytes: p_name = f"{namespace}.{name}" if namespace is not None else name p_sample_rate = f"|@{sample_rate}" if sample_rate != 1 else "" p_tags = build_tags(tags) p_tags = f"|#{p_tags}" if p_tags else "" return f"{p_name}:{value}|{type_.value}{p_sample_rate}{p_tags}".encode("utf-8") def build_tags(tags: typedefs.MTags) -> str: if not tags: return "" return ",".join(f"{k}:{v}" for k, v in tags.items()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/py.typed0000644000000000000000000000000700000000000015331 0ustar00Marker ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/aiodogstatsd/typedefs.py0000644000000000000000000000117300000000000016034 0ustar00import enum from typing import Mapping, Union __all__ = ( "CState", "MName", "MNamespace", "MType", "MValue", "MSampleRate", "MTagKey", "MTagValue", "MTags", ) MName = str MNamespace = str MValue = Union[float, int] MSampleRate = Union[float, int] MTagKey = str MTagValue = Union[float, int, str] MTags = Mapping[MTagKey, MTagValue] @enum.unique class MType(enum.Enum): COUNTER = "c" DISTRIBUTION = "d" GAUGE = "g" HISTOGRAM = "h" TIMING = "ms" @enum.unique class CState(enum.IntEnum): CONNECTED = enum.auto() CLOSING = enum.auto() DISCONNECTED = enum.auto() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520730.4439492 aiodogstatsd-0.14.0/pyproject.toml0000644000000000000000000000442200000000000014066 0ustar00[tool.black] line-length = 88 target-version = ["py36", "py37", "py38"] include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | \.vscode | _build | buck-out | build | dist )/ ''' [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ # Have to re-enable the standard pragma "pragma: no cover", # Don't complain about missing debug-only code: "def __repr__", "if self.debug", # Don't complain about some magic methods: "def __str__", # Don't complain if tests don't hit defensive assertion code: "raise AssertionError", "raise NotImplementedError", # Don't complain if non-runnable code isn't run: "if 0:", "if __name__ == .__main__.:" ] ignore_errors = true [tool.isort] combine_as_imports = true profile = "black" sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" skip = ".eggs,.venv,venv" [tool.poetry] name = "aiodogstatsd" version = "0.14.0" description = "An asyncio-based client for sending metrics to StatsD with support of DogStatsD extension" authors = [ "Nikita Grishko " ] license = "MIT" readme = "README.md" homepage = "https://github.com/Gr1N/aiodogstatsd" repository = "https://github.com/Gr1N/aiodogstatsd" documentation = "https://gr1n.github.io/aiodogstatsd" keywords = ["asyncio", "statsd", "statsd-client", "statsd-metrics", "dogstatsd"] classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules" ] [tool.poetry.dependencies] python = "^3.6" aiohttp = { version = ">=3.0.0", optional = true } sanic = { version = ">=20.3.0", optional = true } starlette = { version = ">=0.13.0", optional = true } [tool.poetry.dev-dependencies] async-asgi-testclient = ">=1.4" black = { version = ">=18.9b0", allow-prereleases = true } coverage = { version = ">=5.0", extras = ["toml"] } flake8 = ">=3.7.6" flake8-bugbear = ">=18.8.0" isort = ">=5.2" mkdocs-material = ">=4.6" mypy = ">=0.670" pytest = ">=4.3.0" pytest-asyncio = ">=0.10.0" pytest-cov = ">=2.6.1" pytest-mock = ">=1.10.1" pytest-mockservers = ">=0.4.0" pytest-timeout = ">=1.3" uvicorn = ">=0.11.2" yarl = ">=1.3.0" [tool.poetry.extras] aiohttp = ["aiohttp"] sanic = ["sanic"] starlette = ["starlette"] [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605520816.639778 aiodogstatsd-0.14.0/setup.py0000644000000000000000000000641300000000000012666 0ustar00# -*- coding: utf-8 -*- from setuptools import setup packages = \ ['aiodogstatsd', 'aiodogstatsd.contrib'] package_data = \ {'': ['*']} extras_require = \ {'aiohttp': ['aiohttp>=3.0.0'], 'sanic': ['sanic>=20.3.0'], 'starlette': ['starlette>=0.13.0']} setup_kwargs = { 'name': 'aiodogstatsd', 'version': '0.14.0', 'description': 'An asyncio-based client for sending metrics to StatsD with support of DogStatsD extension', 'long_description': '# aiodogstatsd\n\n[![Build Status](https://github.com/Gr1N/aiodogstatsd/workflows/default/badge.svg)](https://github.com/Gr1N/aiodogstatsd/actions?query=workflow%3Adefault) [![codecov](https://codecov.io/gh/Gr1N/aiodogstatsd/branch/master/graph/badge.svg)](https://codecov.io/gh/Gr1N/aiodogstatsd) ![PyPI](https://img.shields.io/pypi/v/aiodogstatsd.svg?label=pypi%20version) ![PyPI - Downloads](https://img.shields.io/pypi/dm/aiodogstatsd.svg?label=pypi%20downloads) ![GitHub](https://img.shields.io/github/license/Gr1N/aiodogstatsd.svg)\n\nAn asyncio-based client for sending metrics to StatsD with support of [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/) extension.\n\nLibrary fully tested with [statsd_exporter](https://github.com/prometheus/statsd_exporter) and supports `gauge`, `counter`, `histogram`, `distribution` and `timing` types.\n\n`aiodogstatsd` client by default uses _9125_ port. It\'s a default port for [statsd_exporter](https://github.com/prometheus/statsd_exporter) and it\'s different from _8125_ which is used by default in StatsD and [DataDog](https://www.datadoghq.com/). Initialize the client with the proper port you need if it\'s different from _9125_.\n\n## Installation\n\nJust type:\n\n```sh\n$ pip install aiodogstatsd\n```\n\n## At a glance\n\nJust simply use client as a context manager and send any metric you want:\n\n```python\nimport asyncio\n\nimport aiodogstatsd\n\n\nasync def main():\n async with aiodogstatsd.Client() as client:\n client.increment("users.online")\n\n\nasyncio.run(main())\n```\n\nPlease follow [documentation](https://gr1n.github.io/aiodogstatsd) or look at [`examples/`](https://github.com/Gr1N/aiodogstatsd/tree/master/examples) directory to find more examples of library usage, e.g. integration with [`AIOHTTP`](https://aiohttp.readthedocs.io/), [`Sanic`](https://sanicframework.org/) or [`Starlette`](https://www.starlette.io) frameworks.\n\n## Contributing\n\nTo work on the `aiodogstatsd` codebase, you\'ll want to clone the project locally and install the required dependencies via [poetry](https://poetry.eustace.io):\n\n```sh\n$ git clone git@github.com:Gr1N/aiodogstatsd.git\n$ make install\n```\n\nTo run tests and linters use command below:\n\n```sh\n$ make lint && make test\n```\n\nIf you want to run only tests or linters you can explicitly specify which test environment you want to run, e.g.:\n\n```sh\n$ make lint-black\n```\n\n## License\n\n`aiodogstatsd` is licensed under the MIT license. See the license file for details.\n', 'author': 'Nikita Grishko', 'author_email': 'gr1n@protonmail.com', 'maintainer': None, 'maintainer_email': None, 'url': 'https://github.com/Gr1N/aiodogstatsd', 'packages': packages, 'package_data': package_data, 'extras_require': extras_require, 'python_requires': '>=3.6,<4.0', } setup(**setup_kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605520816.6402292 aiodogstatsd-0.14.0/PKG-INFO0000644000000000000000000000702400000000000012250 0ustar00Metadata-Version: 2.1 Name: aiodogstatsd Version: 0.14.0 Summary: An asyncio-based client for sending metrics to StatsD with support of DogStatsD extension Home-page: https://github.com/Gr1N/aiodogstatsd License: MIT Keywords: asyncio,statsd,statsd-client,statsd-metrics,dogstatsd Author: Nikita Grishko Author-email: gr1n@protonmail.com Requires-Python: >=3.6,<4.0 Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: aiohttp Provides-Extra: sanic Provides-Extra: starlette Requires-Dist: aiohttp (>=3.0.0); extra == "aiohttp" Requires-Dist: sanic (>=20.3.0); extra == "sanic" Requires-Dist: starlette (>=0.13.0); extra == "starlette" Project-URL: Documentation, https://gr1n.github.io/aiodogstatsd Project-URL: Repository, https://github.com/Gr1N/aiodogstatsd Description-Content-Type: text/markdown # aiodogstatsd [![Build Status](https://github.com/Gr1N/aiodogstatsd/workflows/default/badge.svg)](https://github.com/Gr1N/aiodogstatsd/actions?query=workflow%3Adefault) [![codecov](https://codecov.io/gh/Gr1N/aiodogstatsd/branch/master/graph/badge.svg)](https://codecov.io/gh/Gr1N/aiodogstatsd) ![PyPI](https://img.shields.io/pypi/v/aiodogstatsd.svg?label=pypi%20version) ![PyPI - Downloads](https://img.shields.io/pypi/dm/aiodogstatsd.svg?label=pypi%20downloads) ![GitHub](https://img.shields.io/github/license/Gr1N/aiodogstatsd.svg) An asyncio-based client for sending metrics to StatsD with support of [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/) extension. Library fully tested with [statsd_exporter](https://github.com/prometheus/statsd_exporter) and supports `gauge`, `counter`, `histogram`, `distribution` and `timing` types. `aiodogstatsd` client by default uses _9125_ port. It's a default port for [statsd_exporter](https://github.com/prometheus/statsd_exporter) and it's different from _8125_ which is used by default in StatsD and [DataDog](https://www.datadoghq.com/). Initialize the client with the proper port you need if it's different from _9125_. ## Installation Just type: ```sh $ pip install aiodogstatsd ``` ## At a glance Just simply use client as a context manager and send any metric you want: ```python import asyncio import aiodogstatsd async def main(): async with aiodogstatsd.Client() as client: client.increment("users.online") asyncio.run(main()) ``` Please follow [documentation](https://gr1n.github.io/aiodogstatsd) or look at [`examples/`](https://github.com/Gr1N/aiodogstatsd/tree/master/examples) directory to find more examples of library usage, e.g. integration with [`AIOHTTP`](https://aiohttp.readthedocs.io/), [`Sanic`](https://sanicframework.org/) or [`Starlette`](https://www.starlette.io) frameworks. ## Contributing To work on the `aiodogstatsd` codebase, you'll want to clone the project locally and install the required dependencies via [poetry](https://poetry.eustace.io): ```sh $ git clone git@github.com:Gr1N/aiodogstatsd.git $ make install ``` To run tests and linters use command below: ```sh $ make lint && make test ``` If you want to run only tests or linters you can explicitly specify which test environment you want to run, e.g.: ```sh $ make lint-black ``` ## License `aiodogstatsd` is licensed under the MIT license. See the license file for details.