diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c6a60c0d..3dfb8759 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,7 +3,7 @@ name: Python package on: [push, pull_request, workflow_dispatch] env: - release-python-version: "3.10" + release-python-version: "3.11" jobs: test: @@ -81,7 +81,7 @@ jobs: name: distribution-packages path: dist/ - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/MANIFEST.in b/MANIFEST.in index 3585101e..06520cb1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,3 +28,5 @@ exclude .gitmodules exclude .pre-commit-config.yaml exclude .github_changelog_generator exclude .dockerignore + +prune docs/_build diff --git a/docs/api/cli.rst b/docs/api/cli.rst index 6a3dad2a..9d42980c 100644 --- a/docs/api/cli.rst +++ b/docs/api/cli.rst @@ -17,11 +17,11 @@ Decorators ---------- .. automodule:: metricq.cli.decorator - :members: metricq_command, metricq_metric_option, metricq_server_option, metricq_syslog_option, metricq_token_option, + :members: command, metric_option, server_option, syslog_option, token_option, -Parameter ---------- +Parameters +---------- For you convenience, we provide a set of custom parameter types that you can use as custom types in your click option definitions. .. automodule:: metricq.cli.params diff --git a/docs/conf.py b/docs/conf.py index 561755a3..c0d31e88 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "dateutil": ("https://dateutil.readthedocs.io/en/stable/", None), - "aio_pika": ("https://aio-pika.readthedocs.io/en/latest/", None), + "aio_pika": ("https://docs.aio-pika.com/", None), } diff --git a/docs/howto/project-structure.rst b/docs/howto/project-structure.rst index eb9d1f99..b911d0ce 100644 --- a/docs/howto/project-structure.rst +++ b/docs/howto/project-structure.rst @@ -196,11 +196,11 @@ code at runtime: .. code-block:: toml - # in pythonproject.toml + # in pyproject.toml [build-system] requires = [ - ..., # other build dependencies here + # ..., other build dependencies here "setuptools_scm[toml]~=6.0", ] diff --git a/docs/upgrading.rst b/docs/upgrading.rst index d86f2a6f..0fd58c64 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -190,3 +190,19 @@ Renaming of setup.py options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The user option ``out-dir`` has been renamed to ``package-dir``. + +`5.x` → `6.0` +------------- + +Renaming of CLI decorators +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The docorators of the CLI feature have been renamed. + +- `metricq_syslog_option` -> :func:`cli.decorator.syslog_option` +- `metricq_server_option` -> :func:`cli.decorator.server_option` +- `metricq_token_option` -> :func:`cli.decorator.token_option` +- `metricq_metric_option` -> :func:`cli.decorator.metric_option` +- `metricq_command` -> :func:`cli.decorator.command` + + diff --git a/examples/metricq_client.py b/examples/metricq_client.py index 7108078b..3535f9ac 100755 --- a/examples/metricq_client.py +++ b/examples/metricq_client.py @@ -44,7 +44,6 @@ import aiomonitor # type: ignore import metricq -from metricq.cli import metricq_command logger = metricq.get_logger() @@ -59,7 +58,7 @@ async def run(server: str, token: str) -> None: await client.stopped() -@metricq_command(default_token="client-py-example") +@metricq.cli.command(default_token="client-py-example") def main(server: str, token: str) -> None: asyncio.run(run(server, token)) diff --git a/examples/metricq_get_history.py b/examples/metricq_get_history.py index 6fe3ef83..5f93d5f1 100755 --- a/examples/metricq_get_history.py +++ b/examples/metricq_get_history.py @@ -34,11 +34,8 @@ import click import metricq -from metricq.cli import metricq_command -from metricq.cli.decorator import metricq_metric_option -from metricq.logging import get_logger -logger = get_logger() +logger = metricq.get_logger() async def aget_history( @@ -91,8 +88,8 @@ async def aget_history( click.echo(aggregate) -@metricq_command(default_token="history-py-dummy") -@metricq_metric_option() +@metricq.cli.command(default_token="history-py-dummy") +@metricq.cli.metric_option() @click.option("--list-metrics", is_flag=True) @click.option("--list-metadata", is_flag=True) def get_history( diff --git a/examples/metricq_get_history_raw.py b/examples/metricq_get_history_raw.py index cc1205de..3f6da822 100755 --- a/examples/metricq_get_history_raw.py +++ b/examples/metricq_get_history_raw.py @@ -33,8 +33,6 @@ import click import metricq -from metricq.cli import metricq_command -from metricq.cli.decorator import metricq_metric_option from metricq.history_client import HistoryRequestType @@ -73,8 +71,8 @@ async def aget_history(server: str, token: str, metric: str) -> None: await client.stop(None) -@metricq_command(default_token="history-py-dummy") -@metricq_metric_option() +@metricq.cli.command(default_token="history-py-dummy") +@metricq.cli.metric_option() def get_history(server: str, token: str, metric: str) -> None: asyncio.run(aget_history(server, token, metric)) diff --git a/examples/metricq_pandas.py b/examples/metricq_pandas.py index a3b0e2fc..e8182387 100755 --- a/examples/metricq_pandas.py +++ b/examples/metricq_pandas.py @@ -33,8 +33,6 @@ import click import metricq -from metricq.cli import metricq_command -from metricq.cli.decorator import metricq_metric_option from metricq.pandas import PandasHistoryClient logger = metricq.get_logger() @@ -73,8 +71,8 @@ async def aget_history(server: str, token: str, metric: str) -> None: click.echo("----------") -@metricq_command(default_token="history-py-dummy") -@metricq_metric_option(default="example.quantity") +@metricq.cli.command(default_token="history-py-dummy") +@metricq.cli.metric_option(default="example.quantity") def get_history(server: str, token: str, metric: str) -> None: asyncio.run(aget_history(server, token, metric)) diff --git a/examples/metricq_sink.py b/examples/metricq_sink.py index 980e75ab..7fdf67e1 100755 --- a/examples/metricq_sink.py +++ b/examples/metricq_sink.py @@ -33,11 +33,8 @@ import metricq from metricq import Metric -from metricq.cli import metricq_command -from metricq.cli.decorator import metricq_metric_option -from metricq.logging import get_logger -logger = get_logger() +logger = metricq.get_logger() # To implement a MetricQ Sink, subclass metricq.Sink @@ -70,8 +67,8 @@ async def on_data( ) -@metricq_command(default_token="sink-py-dummy") -@metricq_metric_option(multiple=True) +@metricq.cli.command(default_token="sink-py-dummy") +@metricq.cli.metric_option(multiple=True) def source(server: str, token: str, metric: list[Metric]) -> None: # Initialize the DummySink class with a list of metrics given on the # command line. diff --git a/examples/metricq_source.py b/examples/metricq_source.py index edd9e635..42baf2a5 100755 --- a/examples/metricq_source.py +++ b/examples/metricq_source.py @@ -31,10 +31,8 @@ from typing import Any import metricq -from metricq.cli import metricq_command -from metricq.logging import get_logger -logger = get_logger() +logger = metricq.get_logger() class DummySource(metricq.IntervalSource): @@ -65,7 +63,7 @@ async def update(self) -> None: ) -@metricq_command(default_token="source-py-dummy") +@metricq.cli.command(default_token="source-py-dummy") def source(server: str, token: str) -> None: src = DummySource(token=token, url=server) src.run() diff --git a/examples/metricq_synchronous_source.py b/examples/metricq_synchronous_source.py index 930cb984..58ab138a 100755 --- a/examples/metricq_synchronous_source.py +++ b/examples/metricq_synchronous_source.py @@ -33,14 +33,13 @@ import random import time +import metricq from metricq import SynchronousSource, Timestamp -from metricq.cli import metricq_command -from metricq.logging import get_logger -logger = get_logger() +logger = metricq.get_logger() -@metricq_command(default_token="source-py-dummy") +@metricq.cli.command(default_token="source-py-dummy") def synchronous_source(server: str, token: str) -> None: ssource = SynchronousSource(token=token, url=server) ssource.declare_metrics( diff --git a/metricq/__init__.py b/metricq/__init__.py index ad2b5010..560ff864 100644 --- a/metricq/__init__.py +++ b/metricq/__init__.py @@ -52,6 +52,11 @@ ) from .version import __version__ +try: + from . import cli +except ImportError: + pass + # Please keep sorted alphabetically to avoid merge conflicts __all__ = [ "Agent", @@ -76,4 +81,5 @@ "Timestamp", "TimeValue", "__version__", + "cli", ] diff --git a/metricq/cli/__init__.py b/metricq/cli/__init__.py index c23a59dc..4ceedccc 100644 --- a/metricq/cli/__init__.py +++ b/metricq/cli/__init__.py @@ -1,9 +1,9 @@ from .decorator import ( - metricq_command, - metricq_metric_option, - metricq_server_option, - metricq_syslog_option, - metricq_token_option, + command, + metric_option, + server_option, + syslog_option, + token_option, ) from .params import ( ChoiceParam, @@ -21,9 +21,9 @@ "TemplateStringParam", "TimestampParam", "MetricParam", - "metricq_command", - "metricq_metric_option", - "metricq_server_option", - "metricq_syslog_option", - "metricq_token_option", + "command", + "metric_option", + "server_option", + "syslog_option", + "token_option", ] diff --git a/metricq/cli/decorator.py b/metricq/cli/decorator.py index 6a079e79..942c9abd 100644 --- a/metricq/cli/decorator.py +++ b/metricq/cli/decorator.py @@ -1,6 +1,6 @@ import logging import sys -from typing import Any, Callable, Optional, TypeVar, Union, cast +from typing import Any, Callable, List, Optional, TypeVar, Union, cast import click import click_log # type: ignore @@ -24,19 +24,24 @@ FC = TypeVar("FC", bound=Union[Callable[..., Any], click.Command]) -def metricq_syslog_option() -> Callable[[FC], FC]: +def syslog_option() -> Callable[[FC], FC]: """ Exposes the -\\-syslog option as a click param. The program will try read the 'token' from the click params. - if the token is not set, the default value of 'metricq.program' will be used. - That's why the @metricq_syslog_option should be the 2nd decorator in the chain. + If the token is not set, the default value of 'metricq.program' will be + used. That's why the @syslog_option should be the 2nd decorator in the + chain. - It is recommended to use the :py:func:`~metricq.cli.decorator.metricq_command` decorator instead of using this - function directly. + It is recommended to use the :py:func:`~metricq.cli.decorator.command` + decorator instead of using this decorator directly. """ - def enable_syslog(ctx: Context, param: Any | None, value: Optional[str]) -> None: + def enable_syslog( + ctx: Context, + param: Any | None, + value: Optional[str], + ) -> None: if value is not None: logger = get_logger() if value == "": @@ -50,8 +55,9 @@ def enable_syslog(ctx: Context, param: Any | None, value: Optional[str]) -> None return option( "--syslog", - help="Enable syslog logging by specifying the a Unix socket or host:port for the logger. If --syslog is set " - "but no value is specified, the default of localhost:514 will be used.", + help="""Enable syslog logging by specifying the a Unix socket or + host:port for the logger. If --syslog is set but no value is + specified, the default of localhost:514 will be used.""", callback=enable_syslog, expose_value=False, is_flag=False, @@ -59,12 +65,13 @@ def enable_syslog(ctx: Context, param: Any | None, value: Optional[str]) -> None ) -def metricq_server_option() -> Callable[[FC], FC]: +def server_option() -> Callable[[FC], FC]: """ - Allows the User to provide a -\\-server option. This option has no input validation and therefore can be any string. + Allows the User to provide a -\\-server option. This option has no input + validation and therefore can be any string. - It is recommended to use the :py:func:`~metricq.cli.decorator.metricq_command` decorator instead - of using this function directly. + It is recommended to use the :py:func:`~metricq.cli.decorator.command` + decorator instead of using this function directly. """ return option( @@ -76,13 +83,13 @@ def metricq_server_option() -> Callable[[FC], FC]: ) -def metricq_token_option(default: str) -> Callable[[FC], FC]: +def token_option(default: str) -> Callable[[FC], FC]: """ - Allows the User to provide a -\\-metric option. The input must follow the specification provided - `here `_. + Allows the User to provide a -\\-metric option. The input must follow the + specification provided `here `_. - It is recommended to use the :py:func:`~metricq.cli.decorator.metricq_command` decorator instead of using this - function directly. + It is recommended to use the :py:func:`~metricq.cli.decorator.command` + decorator instead of using this function directly. """ return option( "--token", @@ -94,37 +101,41 @@ def metricq_token_option(default: str) -> Callable[[FC], FC]: ) -def metricq_metric_option( - default: Optional[str] = None, multiple: bool = False, required: bool = False +def metric_option( + default: Optional[str | List[str]] = None, + multiple: bool = False, + required: bool = False, ) -> Callable[[FC], FC]: """ - The metric option can be used to select one or more metrics the program should use. - The metric can be set with the -\\-metric or -m parameter. If no default value is set, the parameter is required. - The Metric syntax is specified by the :py:class:`~metricq.cli.params.MetricParam`. + The metric option can be used to select one or more metrics the program + should use. The metric can be set with the -\\-metric or -m parameter. + If no default value is set, the parameter is required. The Metric syntax is + specified by the :py:class:`~metricq.cli.params.MetricParam`. Args: - default: The default metric. Defaults to `None`. You can only set one default, even if the program allows - multiple inputs. - multiple: If `True`, allows multiple metrics to be specified. Defaults to `False`. - required: If `True`, makes the -\\-metric option required. Defaults to `False`. If required is set and no - default is provided, at least one metric input is required. + default: The default metric. Defaults to `None`. + multiple: If `True`, allows multiple metrics to be specified. Defaults + to `False`. + required: If `True`, makes the -\\-metric option required. Defaults to + `False`. If required is set and no default is provided, at + least one metric input is required. **Example**:: - @metricq_command(default_token="example.program") - @metricq_metricq_option(required=true, default="example.metric") - def metric_example( + @metricq.cli.command(default_token="example.program") + @metricq.cli.metric_option(required=true, default="example.metric") + def example_cli_tool( server: str, token: str, metric: str ) -> None: - # Initialize the DummySink class with a list of metrics given on the - # command line. + # Initialize the DummySink class with a list of metrics given on + # the command line. sink = DummySink(metrics=metric, token=token, url=server) - # Run the sink. This call will block until the connection is closed. + # Run the sink. This call will block until the connection is closed sink.run() - @metricq_command(default_token="example.program") - @metricq_metricq_option(required=true, multiple=True) # <-- multiple is set + @metricq.cli.command(default_token="example.program") + @metricq.cli.metric_option(required=true, multiple=True) def multi_metric_example( server: str, token: str, metric: List[str] ) -> None: @@ -132,8 +143,21 @@ def multi_metric_example( sink.run() """ - response_default = default if (default is None or not multiple) else [default] - help = "Use the -–metric / -m parameter to specify which metric the program should use." + response_default: Optional[str | List[str]] + if default is None: + response_default = None + elif multiple: + if isinstance(default, list): + response_default = default + else: + response_default = [default] + else: + assert isinstance(default, str) + response_default = default + + help = """Use the -–metric / -m parameter to specify which metric the + program should use. + """ if multiple: help += " Can be used multiple times to specify several metrics." @@ -158,7 +182,7 @@ def get_metric_command_logger() -> logging.Logger: return logger -def metricq_command( +def command( default_token: str, client_version: str | None = None ) -> Callable[[FC], click.Command]: """Standardized wrapper for click commands @@ -170,27 +194,34 @@ def metricq_command( Returns: Callable[[FC], click.Command]: click command - The :py:func:`~metricq.cli.wrapper.metricq_command` is the first parameter of any given click/cli command. The main purpose is to provide the most used parameters. - These parameters are 'server' and 'token'. + The :py:func:`~metricq.cli.wrapper.command` is the first parameter of any + given click/cli command. The main purpose is to provide the most used + parameters. These parameters are 'server' and 'token'. - -\\-server: - The Server param is used to specify the amqp url of the Network. for example: amqp://localhost/ + The server parameter is used to specify the URL of the Network. + For example: amqp://localhost/ - The server param can be set using the environment variable METRICQ_SERVER or adding the --server {value} option to the cli command + The server parameter can also be set by using the environment variable + METRICQ_SERVER or the SERVER variable in the .metricq environment file - -\\-token: - The Token is used to identify each program on the metricq network. for example: sink-py-dummy + The Token is used to identify each program on the metricq network. + For example: sink-py-dummy - The token param can be set using the environment variable METRICQ_TOKEN or adding the --token {value} option - to the cli command + The token param can be set using the environment variable METRICQ_TOKEN + or the TOKEN variable in the .metricq environment file - -\\-syslog: - The Syslog param is used to enable syslog. It can be used with or without parameter. + The Syslog param is used to enable syslog. It can be used with or + without parameter. - If used without parameter (for example: ``metricq-check --syslog`` ) the Syslog will default to localhost:514. + If used without parameter (for example: ``metricq-check --syslog`` ) + the Syslog will default to localhost:514. - You can also specify a Unix socket (for example: /dev/log) or a custom host (for example: example.com:514) - by adding the value to the syslog flag (for example: ``metricq-check --syslog example.com:514``) + You can also specify a Unix socket (for example: /dev/log) or a custom + host (for example: example.com:514) by adding the value to the syslog + flag (for example: ``metricq-check --syslog example.com:514``) Full example: @@ -198,7 +229,7 @@ def metricq_command( **Example**:: - @metricq_command(default_token="source-py-dummy") + @metricq.cli.command(default_token="source-py-dummy") def dummy( server: str, token: str ) -> None: @@ -230,9 +261,9 @@ def dummy( def decorator(func: FC) -> click.Command: return click.version_option(version=client_version)( log_decorator( - metricq_token_option(default_token)( - metricq_server_option()( - metricq_syslog_option()( + token_option(default_token)( + server_option()( + syslog_option()( click.command( context_settings=context_settings, epilog=epilog )(func) diff --git a/metricq/cli/params.py b/metricq/cli/params.py index b687a0c7..0806ed91 100644 --- a/metricq/cli/params.py +++ b/metricq/cli/params.py @@ -56,7 +56,7 @@ class ChoiceParam(Generic[ChoiceType], ParamType): from enum import Enum, auto from click import option - from metricq.cli import metricq_command, ChoiceParam, CommandLineChoice + from metricq.cli import command, ChoiceParam, CommandLineChoice class OutputFormat(CommandLineChoice, Enum): @@ -71,7 +71,7 @@ def default(cls) -> "OutputFormat": FORMAT = ChoiceParam(OutputFormat, "format") - @metricq_command(default_token="example.program") + @command(default_token="example.program") @option( "--format", type=FORMAT, @@ -94,7 +94,11 @@ def __init__(self, cls: Type[ChoiceType], name: str): self.cls = cls self.name = name - def get_metavar(self, param: click.Parameter) -> str: + def get_metavar( + self, + param: click.Parameter, + ctx: Optional[Context] = None, + ) -> str: return f"({'|'.join(self.cls.as_choice_list())})" def convert( @@ -123,8 +127,8 @@ class DurationParam(ParamType): """ A custom parameter type for Click, enabling Time values. - Accepts the following string inputs - - [] eg: + Accepts the following string inputs: + - [], e.g.: 10s The following units are supported: - 's' / 'seconds[s]' for seconds @@ -139,11 +143,11 @@ class DurationParam(ParamType): from click import option from metricq import Timedelta - from metricq.cli import metricq_command, DurationParam + from metricq.cli import command, DurationParam TIMEOUT = DurationParam(default=None) - @metricq_command(default_token="example.program") + @command(default_token="example.program") @option( "--timeout", type=TIMEOUT, @@ -192,21 +196,21 @@ class TimestampParam(ParamType): """ Convert strings to :py:class:`metricq.Timestamp` objects. - Accepts the following string inputs - - ISO-8601 timestamp (with timezone) - - Past Duration, e.g., '-10h' from now - - Posix timestamp, float seconds since 1.1.1970 midnight. (UTC) - - 'now' - - 'epoch', i.e., 1.1.1970 midnight + Accepts the following string inputs: + - ISO-8601 timestamp (with timezone) + - Past Duration, e.g., '-10h' from now + - Posix timestamp, float seconds since 1.1.1970 midnight. (UTC) + - 'now' + - 'epoch', i.e., 1.1.1970 midnight **Example**:: from click import option - from metricq.cli import metricq_command, TimestampParam + from metricq.cli import command, TimestampParam - @metricq_command(default_token="example.program") + @command(default_token="example.program") @option( "--start", type=TimestampParam(), @@ -271,10 +275,10 @@ class TemplateStringParam(ParamType): from click import option - from metricq.cli import metricq_command, TemplateStringParam + from metricq.cli import command, TemplateStringParam - @metricq_command(default_token="example.program") + @command(default_token="example.program") @option( "--user", type=TemplateStringParam(), @@ -317,8 +321,8 @@ class MetricParam(ParamType): The String has to follow the `Metric Syntax `_ - It is recommended to use the :py:func:`~metricq.cli.decorator.metricq_metric_option` or the - :py:func:`~metricq.cli.decorator.metricq_token_option` decorator instead of using this class directly. + It is recommended to use the :func:`~metricq.cli.decorator.metric_option` + decorator instead of using this class directly. """ diff --git a/metricq/interval_source.py b/metricq/interval_source.py index 6ae380fe..76bcd293 100644 --- a/metricq/interval_source.py +++ b/metricq/interval_source.py @@ -82,7 +82,7 @@ def __init__( if period is None: self._period = None else: - self.period = period # type: ignore # https://github.com/python/mypy/issues/3004 + self.period = period @property def period(self) -> Optional[Timedelta]: diff --git a/metricq/version.py b/metricq/version.py index e6e19e44..a13817a4 100644 --- a/metricq/version.py +++ b/metricq/version.py @@ -27,6 +27,6 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pkg_resources +from importlib.metadata import version -__version__ = pkg_resources.get_distribution("metricq").version +__version__ = version("metricq") diff --git a/setup.cfg b/setup.cfg index d3a24a7c..285e733f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,13 +51,14 @@ typing = %(pandas)s %(examples)s %(test)s + %(cli)s docs = %(pandas)s - sphinx ~= 6.1.3 - sphinx_rtd_theme ~= 1.2.0 - sphinx_autodoc_typehints ~= 1.22.0 + sphinx ~= 8.2.3 + sphinx_rtd_theme ~= 3.0.2 + sphinx_autodoc_typehints ~= 3.2.0 sphinxcontrib-trio ~= 1.1.2 - scanpydoc ~= 0.7.8 + scanpydoc ~= 0.15.4 dev = %(test)s %(lint)s diff --git a/tests/test_interval_source.py b/tests/test_interval_source.py index 6997a647..1fb086c5 100644 --- a/tests/test_interval_source.py +++ b/tests/test_interval_source.py @@ -47,7 +47,7 @@ def test_period_no_reset(interval_source: _TestIntervalSource) -> None: """Currently, the interval source period cannot be reset to None""" with pytest.raises(TypeError, match=r"Setting .* to None is not supported"): - interval_source.period = None + interval_source.period = None # type: ignore def test_period_no_explicit_init() -> None: @@ -66,9 +66,12 @@ def __init__(self) -> None: token="source-interval-test", url="amqps://test.invalid" ) - self.period = None + self.period = None # type: ignore - with pytest.raises(TypeError, match=r"Setting .* to None is not supported"): + with pytest.raises( + TypeError, + match=r"Setting .* to None is not supported", + ): _FaultySource()