diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82421729..8df6850c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,6 +54,7 @@ repos: # - id: double-quote-string-fixer (KEEP DISABLED) # Makes sure files end in a newline and only a newline. - id: end-of-file-fixer + exclude: "^CHANGELOG.rst" # Removes "# -*- coding: utf-8 -*-" on the top of python files. - id: fix-encoding-pragma args: [ "--remove" ] @@ -134,6 +135,8 @@ repos: hooks: - id: pydocstyle args: [ "--ignore=D10,D21,D202" ] + additional_dependencies: + - toml # static type checking with mypy - repo: https://github.com/pre-commit/mirrors-mypy @@ -146,6 +149,7 @@ repos: - types-requests - types-tabulate - types-paramiko + - types-attrs - repo: https://github.com/econchick/interrogate rev: 1.4.0 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75fbeb23..0409476f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,30 @@ This is the changelog of MatrixCtl. You can find the issue tracker on `GitHub `_. .. towncrier release notes start + +0.10.2 (2021-06-24) +=================== + +Features & Improvements +----------------------- + +- Add start/restart switch to the deploy subcommand to start/restart the server + right after the deployment. (`#132 + `_) +- Added the new command ``get-event``, which gets an event by ``event_id`` from + the Database and prints it as JSON. (`#139 + `_) + + +Miscellaneous +------------- + +- Rewritten API handler. (`#136 + `_) +- Fixed: Wrong version while developing in virtual environment. (`#141 + `_) + + 0.10.1 (2021-06-17) =================== @@ -103,3 +127,4 @@ Miscellaneous GitHub `_. (`#61 `_) + diff --git a/README.md b/README.md index 20a0c571..5caa29e8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ usage: matrixctl [-h] [--version] [-d] ... positional arguments: - {adduser,deluser,adduser-jitsi,deluser-jitsi,user,users,purge-history,rooms,delroom,update,upload,deploy,server-notice,start,restart,maintenance,check,version} + {adduser,deluser,adduser-jitsi,deluser-jitsi,user,users,purge-history,rooms,delroom,update,upload,deploy,server-notice,get-event,start,restart,maintenance,check,version} adduser Add a new matrix user deluser Deletes a user adduser-jitsi Add a new jitsi user @@ -39,6 +39,7 @@ positional arguments: upload Upload a file. deploy Provision and deploy server-notice Send a server notice + get-event get an event from the DB start Starts all OCI containers stop Stops all OCI containers restart Restarts all OCI containers (alias for start) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 53bca03d..06b89b4f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,4 +1 @@ -Changelog -========= - .. include:: ../../CHANGELOG.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 2fa8c9ea..27ebe884 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -53,6 +53,8 @@ "sphinx.ext.inheritance_diagram", ] +suppress_warnings: list[str] = ["autosectionlabel.*"] + intersphinx_mapping = { "python": ("https://docs.python.org/3", None), # "numpy": ("https://www.numpy.org/devdocs", None), diff --git a/docs/source/matrixctl.rst b/docs/source/matrixctl.rst index 10ae7903..84506386 100644 --- a/docs/source/matrixctl.rst +++ b/docs/source/matrixctl.rst @@ -143,6 +143,14 @@ server-notice :undoc-members: :show-inheritance: +get-event +--------- + +.. automodule:: matrixctl.get_event + :members: + :undoc-members: + :show-inheritance: + Helpers ======= diff --git a/matrixctl/__init__.py b/matrixctl/__init__.py index 92f5205a..837fa4e3 100644 --- a/matrixctl/__init__.py +++ b/matrixctl/__init__.py @@ -21,13 +21,16 @@ from __future__ import annotations -from importlib.metadata import version from pathlib import Path +from single_source import get_version + __author__: str = "Michael Sasser" __email__: str = "Michael@MichaelSasser.org" -__version__: str = version("matrixctl") +__version__: str = ( + get_version(__name__, Path(__file__).parent.parent) or "Unknown" +) HOME: str = str(Path.home()) diff --git a/matrixctl/__main__.py b/matrixctl/__main__.py index d9a0ee26..0b5274d0 100644 --- a/matrixctl/__main__.py +++ b/matrixctl/__main__.py @@ -35,6 +35,7 @@ from matrixctl.deluser import subparser_deluser from matrixctl.deluser_jitsi import subparser_deluser_jitsi from matrixctl.deploy import subparser_deploy +from matrixctl.get_event import subparser_get_event from matrixctl.maintenance import subparser_maintenance from matrixctl.purge_history import subparser_purge_history from matrixctl.rooms import subparser_rooms @@ -98,6 +99,7 @@ def setup_parser() -> argparse.ArgumentParser: subparser_upload, subparser_deploy, subparser_server_notice, + subparser_get_event, subparser_start, subparser_stop, subparser_restart, # alias for start @@ -130,7 +132,7 @@ def setup_logging(debug_mode: bool) -> None: # %(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s coloredlogs.install( - level="DEBUG" if debug_mode else "INFO", + level="DEBUG" if debug_mode else "WARNING", fmt=( "%(asctime)s %(name)s:%(lineno)d [%(funcName)s] %(levelname)s " "%(message)s" diff --git a/matrixctl/adduser.py b/matrixctl/adduser.py index 1ad54ad6..5f412f27 100644 --- a/matrixctl/adduser.py +++ b/matrixctl/adduser.py @@ -27,7 +27,8 @@ from .errors import InternalResponseError from .handlers.ansible import ansible_run -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .password_helpers import ask_password from .password_helpers import ask_question @@ -103,7 +104,6 @@ def adduser(arg: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) while True: passwd_generated: bool = False @@ -139,13 +139,19 @@ def adduser(arg: Namespace) -> int: "admin": "yes" if arg.admin else "no", }, ) - else: - try: - api.url.path = f"users/@{arg.user}:{toml.get('API','Domain')}" - api.method = "PUT" - api.request({"password": arg.passwd, "admin": arg.admin}) - except InternalResponseError: - logger.error("The User was not added.") + return 0 + + req: RequestBuilder = RequestBuilder( + domain=toml.get("API", "Domain"), + token=toml.get("API", "Token"), + path=f"users/@{arg.user}:{toml.get('API','Domain')}", + data={"password": arg.passwd, "admin": arg.admin}, + method="PUT", + ) + try: + request(req) + except InternalResponseError: + logger.error("The User was not added.") return 0 diff --git a/matrixctl/delroom.py b/matrixctl/delroom.py index e7b7cba6..3fd5073d 100644 --- a/matrixctl/delroom.py +++ b/matrixctl/delroom.py @@ -26,7 +26,8 @@ from argparse import _SubParsersAction as SubParsersAction from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML @@ -77,13 +78,17 @@ def delroom(arg: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.method = "POST" - api.url.path = "purge_room" - api.url.api_version = "v1" + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="purge_room", + method="POST", + api_version="v1", + data={"room_id": arg.RoomID}, + ) try: - api.request({"room_id": arg.RoomID}).json() + request(req).json() except InternalResponseError as e: if "json" in dir(e.payload): try: diff --git a/matrixctl/deluser.py b/matrixctl/deluser.py index 30c66ee8..766f666d 100644 --- a/matrixctl/deluser.py +++ b/matrixctl/deluser.py @@ -26,7 +26,8 @@ from argparse import _SubParsersAction as SubParsersAction from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML @@ -73,12 +74,17 @@ def deluser(arg: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) + + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path=f"deactivate/@{arg.user}:{toml.get('API','Domain')}", + api_version="v1", + method="POST", + data={"erase": True}, + ) try: - api.url.path = f"deactivate/@{arg.user}:{toml.get('API','Domain')}" - api.url.api_version = "v1" - api.method = "POST" - api.request({"erase": True}) + request(req) except InternalResponseError: logger.error("The user was not deleted.") diff --git a/matrixctl/deploy.py b/matrixctl/deploy.py index 3c779742..8a59ab39 100644 --- a/matrixctl/deploy.py +++ b/matrixctl/deploy.py @@ -53,10 +53,17 @@ def subparser_deploy(subparsers: SubParsersAction) -> None: parser: ArgumentParser = subparsers.add_parser( "deploy", help="Provision and deploy" ) + + parser.add_argument( # Done with tags / Does not use matrixctl.start ! + "-s", + "--start", + action="store_true", + help="Start/Restart after the deployment", + ) parser.set_defaults(func=deploy) -def deploy(_: Namespace) -> int: +def deploy(arg: Namespace) -> int: """Deploy the ansible playbook. Parameters @@ -72,9 +79,10 @@ def deploy(_: Namespace) -> int: """ logger.debug("deploy") toml: TOML = TOML() + ansible_run( playbook=toml.get("ANSIBLE", "Playbook"), - tags="setup-all", + tags="setup-all,start" if arg.start else "setup-all", ) return 0 diff --git a/matrixctl/get_event.py b/matrixctl/get_event.py new file mode 100644 index 00000000..7df3798f --- /dev/null +++ b/matrixctl/get_event.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# matrixctl +# Copyright (c) 2020 Michael Sasser +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Use this module to get an event from the Database.""" + +from __future__ import annotations + +import json +import logging + +from argparse import ArgumentParser +from argparse import Namespace +from argparse import _SubParsersAction as SubParsersAction +from base64 import b64encode + +from .handlers.ssh import SSH +from .handlers.ssh import SSHResponse +from .handlers.toml import TOML +from .typing import JsonDict + + +__author__: str = "Michael Sasser" +__email__: str = "Michael@MichaelSasser.org" + + +logger = logging.getLogger(__name__) + +JID_EXT: str = "matrix-jitsi-web" + + +def subparser_get_event(subparsers: SubParsersAction) -> None: + """Create a subparser for the ``matrixctl get-event`` command. + + Parameters + ---------- + subparsers : argparse._SubParsersAction + The object which is returned by + ``parser.add_subparsers()``. + + Returns + ------- + None + + """ + parser: ArgumentParser = subparsers.add_parser( + "get-event", help="get an event from the DB" + ) + parser.add_argument("event_id", help="The event-id") + parser.set_defaults(func=get_event) + + +def get_event(arg: Namespace) -> int: + """Get an Event from the Server. + + It connects via paramiko to the server and runs the psql command provided + by the synapse playbook to run a query on the Database. + + Parameters + ---------- + arg : argparse.Namespace + The ``Namespace`` object of argparse's ``parse_args()`` + + Returns + ------- + err_code : int + Non-zero value indicates error code, or zero on success. + + """ + + toml: TOML = TOML() + address = ( + toml.get("SSH", "Address") + if toml.get("SSH", "Address") + else f"matrix.{toml.get('API', 'Domain')}" + ) + + # Workaround because of "$" through: paramiko - bash - psql + event64 = b64encode(arg.event_id.encode("utf-8")).decode("utf-8") + + query: str = "SELECT json FROM event_json WHERE event_id='$event'" + cmd: str = "/usr/local/bin/matrix-postgres-cli -P pager" + table: str = "synapse" + + command: str = ( + f"event=$(echo '{event64}' | base64 -d -) && " # Workaround + f'sudo {cmd} -d {table} -c "{query}"' + ) + + logger.debug(f"command: {command}") + + with SSH(address, toml.get("SSH", "User"), toml.get("SSH", "Port")) as ssh: + response: SSHResponse = ssh.run_cmd(command, tty=True) + + if not response.stderr: + logger.debug(f"response: {response.stdout}") + if response.stdout: + start: int = response.stdout.find("{") + stop: int = response.stdout.rfind("}") + 1 + + response_parsed: JsonDict = json.loads(response.stdout[start:stop]) + print(json.dumps(response_parsed, indent=4)) + return 0 + print("The response from the Database was empty.") + return 0 + logger.error(f"response: {response.stderr}") + print( + "An error occured during the query. Are you sure, you used the " + "correct event_id?" + ) + return 1 + + +# vim: set ft=python : diff --git a/matrixctl/handlers/api.py b/matrixctl/handlers/api.py index 0172fd30..342cfe61 100644 --- a/matrixctl/handlers/api.py +++ b/matrixctl/handlers/api.py @@ -22,10 +22,10 @@ import json import logging import sys +import typing +import urllib.parse -from typing import Any -from urllib.parse import urlparse - +import attr import requests from matrixctl import __version__ @@ -39,187 +39,24 @@ logger = logging.getLogger(__name__) -def check_type(var: Any, var_name: str, var_type: Any = str) -> None: - """Check the type of an variable in an simplified and unified way.""" - if not isinstance(var, var_type): - raise TypeError( - f"{var_name} needs to be of type {var_type}, you have entered " - f"{type(var)}." - ) - - -class UrlBuilder: +@attr.s(slots=True, auto_attribs=True, repr=False) +class RequestBuilder: """Build the URL for an API request.""" - __slots__ = ( - "__scheme", - "__subdomain", - "__domain", - "__api_path", - "__api_version", - "__path", - ) - - def __init__(self, default_domain: str) -> None: - self.__scheme: str = "https" - self.__subdomain: str = "matrix" - self.__domain: str = default_domain - self.__api_path: str = "_synapse/admin" - self.__api_version: str = "v2" - self.__path: str = "" - - def _scheme(self, scheme: str) -> None: - """Set the scheme. - - Parameters - ---------- - scheme : str - The scheme. - - Returns - ------- - None - - """ - check_type(scheme, "scheme") - self.__scheme = scheme - - def _domain(self, domain: str) -> None: - """Set the domain. - - Parameters - ---------- - domain : str - The domain. - - Returns - ------- - None - - """ - check_type(domain, "domain") - self.__domain = domain - - def _subdomain(self, subdomain: str) -> None: - """Set the sub-domain. - - Parameters - ---------- - subdomain : str - The sub-domain. - - Returns - ------- - None - - """ - check_type(subdomain, "subdomain") - self.__subdomain = subdomain - - def _api_path(self, api_path: str) -> None: - """Set the API path. - - Parameters - ---------- - api_path : str - The API path. - - Returns - ------- - None - - """ - check_type(api_path, "api_path") - self.__api_path = api_path - - def _api_version(self, api_version: str) -> None: - """Set the API version. - - Parameters - ---------- - api_version : str - The API version. - - Returns - ------- - None - - """ - check_type(api_version, "api_version") - self.__api_version = api_version - - def _path(self, path: str) -> None: - """Set the path. - - Parameters - ---------- - path : str - The path. - - Returns - ------- - None - - """ - check_type(path, "path") - self.__path = path - - scheme = property(fset=_scheme) - domain = property(fset=_domain) - subdomain = property(fset=_subdomain) - api_path = property(fset=_api_path) - api_version = property(fset=_api_version) - path = property(fset=_path) - - def build(self) -> str: - """Build the URL. - - Parameters - ---------- - None - - Returns - ------- - url : str - The URL. - - """ - # Url Generation - url: str = ( - f"{self.__scheme}://" - f"{self.__subdomain}{'.' if self.__subdomain else ''}" - f"{self.__domain}" - f"/{self.__api_path}" - f"/{self.__api_version}" - f"/{self.__path}" - ) - logger.debug(f"url (unparsed): {url}") - url = urlparse(url).geturl() - logger.debug(f"url (parsed): {url}") - - return url - - -class API: - - """Handle the REST API connection of synapse.""" - - __slots__ = ( - "token", - "domain", - "json_format", - "url", - "__success_codes", - "session", - "__request", - "__method", - "__params", - "__headers", - ) - - # (*range(200, 208), 226) - RESPONSE_OK: tuple[int, ...] = ( + token: str = attr.ib() + domain: str = attr.ib() + path: str = attr.ib() + scheme: str = "https" + subdomain: str = "matrix" + api_path: str = "_synapse/admin" + api_version: str = "v2" + data: bytes | str | None | dict[str, typing.Any] = None + method: str = "GET" + json: bool = True + params: dict[str, str | int] = {} + headers: dict[str, str] = {} + success_codes: tuple[int, ...] = ( 200, 201, 202, @@ -231,197 +68,139 @@ class API: 226, ) - def __init__( - self, domain: str, token: str, json_format: bool = True - ) -> None: - """Initialize the Api class and checks, if the parameter are there. - - :param api_domain: The API domain (e.g. "domain.tld") - :param api_token: The access token of an admin - :return: ``None`` - """ - - check_type(domain, "domain") - check_type(token, "token") - check_type(json_format, "json_format", bool) - - self.token: str = token - self.json_format: bool = json_format - - self.url: UrlBuilder = UrlBuilder(domain) - self.__success_codes: tuple[int, ...] = self.__class__.RESPONSE_OK - - self.session = requests.Session() - - self.__method: str = "GET" - self.__params: dict[str, str] = {} - self.__headers: dict[str, str] = { - "User-Agent": f"matrixctl{__version__}", - "Authorization": f"Bearer {self.token}", - } - - def _method(self, method: str) -> None: - """Set the request method. + @property + def headers_with_auth(self) -> dict[str, str]: + """Get the headers with bearer token. Parameters ---------- - method : str - Set the method. Possible methods are: - - ``"GET"`` - - ``"POST"`` - - ``"PUT"`` - - ``"DELETE"`` + None Returns ------- - None + headers : dict [str, str] + Headers with auth. token. """ - method = method.upper() - if method not in {"GET", "POST", "PUT", "DELETE"}: - raise ValueError( - 'method needs to be one of {"GET", "POST", "PUT", "DELETE"}, ' - f"you have entered {type(method)}." - ) - self.__method = method + return self.headers | { + "User-Agent": f"matrixctl{__version__}", + "Authorization": f"Bearer {self.token}", + } - def _params(self, params: dict[str, str]) -> None: - """Set the params. + def __str__(self) -> str: + """Build the URL. Parameters ---------- - params : dict [str, str] - Set the params with a dict. + None Returns ------- - None + url : str + The URL. """ - check_type(params, "params", dict) - self.__params.update(params) + url: str = ( + f"{self.scheme}://" + f"{self.subdomain}{'.' if self.subdomain else ''}" + f"{self.domain}" + f"/{self.api_path}" + f"/{self.api_version}" + f"/{self.path}" + ) + url = urllib.parse.urlparse(url).geturl() + return url - def _headers(self, headers: dict[str, str]) -> None: - """Set the headers. + def __repr__(self) -> str: + """Get a string representation of this class. Parameters ---------- - headers : dict [str, str] - Set the headers with a dict. + None Returns ------- - None + url : str + Data of this class in string representation. """ - check_type(headers, "headers", dict) - - if self.json_format: - self.__headers["Content-Type"] = "application/json" + return ( + f"{self.__class__.__qualname__}({self.method} {self.__str__()} {{" + f"headers={self.headers}, params={self.params}, data=" + f"{'[binary]' if isinstance(self.data, bytes) else self.data} " + f"success_codes={self.success_codes}, json={self.json}, " + f"token=[redacted (length={len(self.token)})])}}" + ) - self.__headers.update(headers) - def _success_codes(self, codes: tuple[int]) -> None: - """Set the success code(s). +def request(req: RequestBuilder) -> requests.Response: + """Send an request to the synapse API and receive a response. - Parameters - ---------- - codes : tuple of str - Set the success code(s) as tuple of strings. + Parameters + ---------- + req : matrixctl.handlers.api.RequestBuilder + An instance of an RequestBuilder - Returns - ------- - None - - """ - check_type(codes, "codes", dict) - self.__success_codes = codes + Returns + ------- + response : requests.Response + Returns the response - method = property(fset=_method) - params = property(fset=_params) - headers = property(fset=_headers) - success_codes = property(fset=_success_codes) + """ + data = req.data + if req.json and data is not None: + data = json.dumps(req.data) - def request( - self, data: bytes | str | None | dict[str, Any] = None - ) -> requests.Response: - """Send an request to the synapse API and receive a response. + logger.debug(repr(req)) - Parameters - ---------- - data : bytes or str or dict [str, Any], optional, default=None - The payload of the request. - - Returns - ------- - response : requests.Response - Returns the response + response = requests.Session().request( + method=req.method, + data=data, + url=str(req), + params=req.params, + headers=req.headers_with_auth, + allow_redirects=False, + ) - """ - url: str = self.url.build() - - if self.json_format and data is not None: - data = json.dumps(data) - - debug_headers = self.__headers.copy() - # len("Bearer ") = 7 - debug_headers[ - "Authorization" - ] = f"HIDDEN (Length={len(self.__headers['Authorization']) - 7})" - logger.debug(f"Method: {self.__method}") - logger.debug(f"Headers: {debug_headers}") - logger.debug(f"Params: {self.__params}") - if not isinstance(data, bytes): # TODO: Bytes debug - logger.debug(f"Data: {data}") - - response = self.session.request( - method=self.__method, - data=data, - url=url, - params=self.__params, - headers=self.__headers, - allow_redirects=False, + if response.status_code == 302: + logger.critical( + "The api request resulted in an redirect (302). " + "This indicates, that the API might have changed, or your " + "playbook is misconfigured.\n" + "Please make sure your installation of matrixctl is " + "up-to-date and your vars.yml contains:\n\n" + "matrix_nginx_proxy_proxy_matrix_client_redirect_root_uri_to" + '_domain: ""' ) - - if response.status_code == 302: - logger.critical( - "The api request resulted in an redirect (302). " - "This indicates, that the API might have changed, or your " - "playbook is misconfigured.\n" - "Please make sure your installation of matrixctl is " - "up-to-date and your vars.yml contains:\n\n" - "matrix_nginx_proxy_proxy_matrix_client_redirect_root_uri_to" - '_domain: ""' - ) - sys.exit(1) - if response.status_code == 404: - logger.critical( - "You need to make sure, that your vars.yml contains the " - "following excessive long line:\n\n" - "matrix_nginx_proxy_proxy_matrix_client_api_forwarded_" - "location_synapse_admin_api_enabled: true" - ) - sys.exit(1) - - logger.debug(f"{response.json()=}") - - logger.debug(f"{response.status_code=}") - if response.status_code not in self.__success_codes: - try: - if response.json()["errcode"] == "M_UNKNOWN_TOKEN": - logger.critical( - "The server rejected your access-token. " - "Please make sure, your access-token is correct " - "and up-to-date. Your access-token will change every " - "time, you log out." - ) - sys.exit(1) - except Exception: # pylint: disable=broad-except - pass - - raise InternalResponseError(payload=response) - - return response + sys.exit(1) + if response.status_code == 404: + logger.critical( + "You need to make sure, that your vars.yml contains the " + "following excessive long line:\n\n" + "matrix_nginx_proxy_proxy_matrix_client_api_forwarded_" + "location_synapse_admin_api_enabled: true" + ) + sys.exit(1) + + logger.debug(f"{response.json()=}") + + logger.debug(f"{response.status_code=}") + if response.status_code not in req.success_codes: + try: + if response.json()["errcode"] == "M_UNKNOWN_TOKEN": + logger.critical( + "The server rejected your access-token. " + "Please make sure, your access-token is correct " + "and up-to-date. Your access-token will change every " + "time, you log out." + ) + sys.exit(1) + except Exception: # pylint: disable=broad-except + pass + + raise InternalResponseError(payload=response) + + return response # vim: set ft=python : diff --git a/matrixctl/handlers/ssh.py b/matrixctl/handlers/ssh.py index ac4c00da..0990bcec 100644 --- a/matrixctl/handlers/ssh.py +++ b/matrixctl/handlers/ssh.py @@ -111,13 +111,15 @@ def __str_from(f: ChannelFile) -> str | None: except OSError: return None - def run_cmd(self, cmd: str) -> SSHResponse: + def run_cmd(self, cmd: str, tty: bool = False) -> SSHResponse: """Run a command on the host machine and receive a response. Parameters ---------- cmd : str The command to run. + tty : bool + Request a pseudo-terminal from the server (default: ``False``) Returns ------- @@ -129,7 +131,10 @@ def run_cmd(self, cmd: str) -> SSHResponse: response: SSHResponse = SSHResponse( # skipcq: BAN-B601 - *[self.__str_from(s) for s in self.__client.exec_command(cmd)] + *[ + self.__str_from(s) + for s in self.__client.exec_command(cmd, get_pty=tty) + ] ) logger.debug(f'SSH Response: "{response}"') diff --git a/matrixctl/handlers/toml.py b/matrixctl/handlers/toml.py index a4c8af26..efb043e6 100644 --- a/matrixctl/handlers/toml.py +++ b/matrixctl/handlers/toml.py @@ -26,6 +26,7 @@ from copy import deepcopy from pathlib import Path from typing import Any +from typing import cast import toml @@ -47,6 +48,7 @@ class TOML: Path("/etc/matrixctl/config"), Path.home() / ".config/matrixctl/config", ] + __instance: TOML | None = None __slots__ = ("__toml",) def __init__(self, path: Path | None = None) -> None: @@ -60,6 +62,25 @@ def __init__(self, path: Path | None = None) -> None: self.__debug_output() + def __new__(cls) -> TOML: # TODO: weakref + """Make TOML a Singelton. + + Parameters + ---------- + cls : matrixctl.handlers.TOML + New instance. + + Returns + ------- + toml_instance : TOML + A new or reused (Singelton) TOML instance. + + """ + if cls.__instance is None: + logger.debug("Creating new TOML instance.") + cls.__instance = cast(TOML, super().__new__(cls)) + return cls.__instance + @staticmethod def __open(paths: list[Path]) -> dict[str, Any]: """Open a TOML file and suppress warnings of the toml module. diff --git a/matrixctl/purge_history.py b/matrixctl/purge_history.py index bf265be3..513030ee 100644 --- a/matrixctl/purge_history.py +++ b/matrixctl/purge_history.py @@ -31,7 +31,8 @@ from time import sleep from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .password_helpers import ask_question from .typing import JsonDict @@ -126,15 +127,18 @@ def handle_purge_status(toml: TOML, purge_id: str) -> int: The response as dict, containing the status. """ - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.path = f"purge_history_status/{purge_id}" - api.url.api_version = "v1" - api.method = "GET" + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path=f"purge_history_status/{purge_id}", + method="GET", + api_version="v1", + ) while True: try: - response: JsonDict = api.request().json() + response: JsonDict = request(req).json() except InternalResponseError: logger.critical( "The purge history request was successful but the status " @@ -227,13 +231,17 @@ def purge_history(arg: Namespace) -> int: toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.path = f"purge_history/{arg.room_id}" - api.url.api_version = "v1" - api.method = "POST" + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path=f"purge_history/{arg.room_id}", + method="POST", + api_version="v1", + data=request_body, + ) try: - response: JsonDict = api.request(request_body).json() + response: JsonDict = request(req).json() except InternalResponseError: logger.critical( "Something went wrong with the request. Please check your data " diff --git a/matrixctl/rooms.py b/matrixctl/rooms.py index 1c2090ec..e1879e90 100644 --- a/matrixctl/rooms.py +++ b/matrixctl/rooms.py @@ -28,7 +28,8 @@ from tabulate import tabulate from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .typing import JsonDict @@ -98,27 +99,30 @@ def rooms(arg: Namespace) -> int: from_room: int = 0 rooms_list: list[JsonDict] = [] - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.path = "rooms" - api.url.api_version = "v1" + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="rooms", + api_version="v1", + ) if arg.number > 0: - api.params = {"limit": arg.number} + req.params["limit"] = arg.number if arg.filter: - api.params = {"search_term": arg.filter} + req.params["search_term"] = arg.filter if arg.reverse: - api.params = {"dir": "b"} + req.params["dir"] = "b" if arg.order_by_size: - api.params = {"order_by": "size"} + req.params["order_by"] = "size" while True: - api.params = {"from": from_room} # from must be in the loop + req.params["from"] = from_room # from must be in the loop try: - lst: JsonDict = api.request().json() + lst: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not get the room table.") diff --git a/matrixctl/server_notice.py b/matrixctl/server_notice.py index c997e2bf..b5562f2a 100644 --- a/matrixctl/server_notice.py +++ b/matrixctl/server_notice.py @@ -26,7 +26,8 @@ from argparse import _SubParsersAction as SubParsersAction from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML @@ -85,20 +86,23 @@ def server_notice(arg: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - request = { - "user_id": (f"@{arg.username}:" f"{toml.get('API', 'Domain')}"), - "content": { - "msgtype": "m.text", - "body": arg.message, + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="send_server_notice", + method="POST", + api_version="v1", + data={ + "user_id": (f"@{arg.username}:" f"{toml.get('API', 'Domain')}"), + "content": { + "msgtype": "m.text", + "body": arg.message, + }, }, - } + ) try: - api.url.path = "send_server_notice" - api.url.api_version = "v1" - api.method = "POST" - api.request(request) + request(req) except InternalResponseError: logger.error("The server notice was not sent.") diff --git a/matrixctl/upload.py b/matrixctl/upload.py index 555761a3..833119be 100644 --- a/matrixctl/upload.py +++ b/matrixctl/upload.py @@ -28,7 +28,8 @@ from pathlib import Path from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .typing import JsonDict @@ -87,15 +88,19 @@ def upload(arg: Namespace) -> int: return 1 toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="upload/", + api_path="_matrix/media", + method="POST", + api_version="r0", + json=False, + headers={"Content-Type": file_type}, + data=file, + ) try: - api.url.api_path = "_matrix/media" - api.url.path = "upload/" - api.url.api_version = "r0" - api.method = "POST" - api.json_format = False - api.headers = {"Content-Type": file_type} - response: JsonDict = api.request(file).json() + response: JsonDict = request(req).json() except InternalResponseError: logger.error("The file was not uploaded.") return 1 diff --git a/matrixctl/user.py b/matrixctl/user.py index cc133fe1..34e84b54 100644 --- a/matrixctl/user.py +++ b/matrixctl/user.py @@ -31,7 +31,8 @@ from tabulate import tabulate from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .print_helpers import human_readable_bool from .typing import JsonDict @@ -230,11 +231,15 @@ def user(arg: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.path = f'users/@{arg.user}:{toml.get("API","Domain")}' + + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path=f'users/@{arg.user}:{toml.get("API","Domain")}', + ) try: - user_dict: JsonDict = api.request().json() + user_dict: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not receive the user information") diff --git a/matrixctl/users.py b/matrixctl/users.py index 40a3229b..ef2b0e0a 100644 --- a/matrixctl/users.py +++ b/matrixctl/users.py @@ -28,7 +28,8 @@ from tabulate import tabulate from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .print_helpers import human_readable_bool from .typing import JsonDict @@ -125,19 +126,24 @@ def users(arg: Namespace) -> int: users_list: list[JsonDict] = [] # ToDo: API bool - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.api_version = "v2" - api.url.path = "users" - api.params = {"guests": "true" if arg.with_guests or arg.all else "false"} - api.params = { - "deactivated": "true" if arg.with_deactivated or arg.all else "false" - } + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="users", + api_version="v2", + params={ + "guests": "true" if arg.with_guests or arg.all else "false", + "deactivated": "true" + if arg.with_deactivated or arg.all + else "false", + }, + ) while True: - api.params = {"from": from_user} # from must be in the loop + req.params["from"] = from_user # from must be in the loop try: - lst: JsonDict = api.request().json() + lst: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not get the user table.") diff --git a/matrixctl/version.py b/matrixctl/version.py index c5e99bbc..409d1a25 100644 --- a/matrixctl/version.py +++ b/matrixctl/version.py @@ -26,7 +26,8 @@ from argparse import _SubParsersAction as SubParsersAction from .errors import InternalResponseError -from .handlers.api import API +from .handlers.api import RequestBuilder +from .handlers.api import request from .handlers.toml import TOML from .typing import JsonDict @@ -73,11 +74,15 @@ def version(_: Namespace) -> int: """ toml: TOML = TOML() - api: API = API(toml.get("API", "Domain"), toml.get("API", "Token")) - api.url.path = "server_version" - api.url.api_version = "v1" + + req: RequestBuilder = RequestBuilder( + token=toml.get("API", "Token"), + domain=toml.get("API", "Domain"), + path="server_version", + api_version="v1", + ) try: - response: JsonDict = api.request().json() + response: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not get the server sersion.") diff --git a/poetry.lock b/poetry.lock index a08d7920..4bfa2d94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,7 +53,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -286,11 +286,11 @@ smmap = ">=3.0.1,<5" [[package]] name = "gitpython" -version = "3.1.17" +version = "3.1.18" description = "Python Git Library" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -387,16 +387,17 @@ tests = ["pytest", "pytest-cov", "pytest-mock"] [[package]] name = "isort" -version = "5.8.0" +version = "5.9.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "jinja2" @@ -454,7 +455,7 @@ python-versions = "*" [[package]] name = "mypy" -version = "0.902" +version = "0.910" description = "Optional static typing for Python" category = "dev" optional = false @@ -801,6 +802,14 @@ urllib3 = ">=1.21.1,<1.27" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "single-source" +version = "0.2.0" +description = "Access to the project version in Python code for PEP 621-style projects" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "six" version = "1.16.0" @@ -1012,9 +1021,17 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] +[[package]] +name = "types-attrs" +version = "19.1.0" +description = "Typing stubs for attrs" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "types-cryptography" -version = "3.3.2" +version = "3.3.3" description = "Typing stubs for cryptography" category = "dev" optional = false @@ -1026,7 +1043,7 @@ types-ipaddress = "*" [[package]] name = "types-enum34" -version = "0.1.5" +version = "0.1.8" description = "Typing stubs for enum34" category = "dev" optional = false @@ -1034,7 +1051,7 @@ python-versions = "*" [[package]] name = "types-ipaddress" -version = "0.1.2" +version = "0.1.5" description = "Typing stubs for ipaddress" category = "dev" optional = false @@ -1042,7 +1059,7 @@ python-versions = "*" [[package]] name = "types-paramiko" -version = "0.1.6" +version = "0.1.7" description = "Typing stubs for paramiko" category = "dev" optional = false @@ -1053,7 +1070,7 @@ types-cryptography = "*" [[package]] name = "types-pkg-resources" -version = "0.1.2" +version = "0.1.3" description = "Typing stubs for pkg_resources" category = "dev" optional = false @@ -1061,7 +1078,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "0.1.11" +version = "2.25.0" description = "Typing stubs for requests" category = "dev" optional = false @@ -1069,7 +1086,7 @@ python-versions = "*" [[package]] name = "types-tabulate" -version = "0.1.0" +version = "0.1.1" description = "Typing stubs for tabulate" category = "dev" optional = false @@ -1077,7 +1094,7 @@ python-versions = "*" [[package]] name = "types-toml" -version = "0.1.2" +version = "0.1.3" description = "Typing stubs for toml" category = "dev" optional = false @@ -1155,7 +1172,7 @@ docs = ["sphinx", "sphinx-autodoc-typehints", "numpydoc", "sphinxcontrib-program [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "23433c99d0a0348194d39831e5b517dec1f16f256aee46b59b9b5831f0cc5bb0" +content-hash = "4396602b3d11cb8ce58b201ebd1d91097590aad3100db2446e9d523e04bb5d8c" [metadata.files] alabaster = [ @@ -1358,8 +1375,8 @@ gitdb = [ {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ - {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, - {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, + {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"}, + {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"}, ] greenlet = [ {file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"}, @@ -1441,8 +1458,8 @@ interrogate = [ {file = "interrogate-1.4.0.tar.gz", hash = "sha256:5fdef4704ee9afff5e7ef5649fc85df4d927853836ef6572776c480307fe4927"}, ] isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, + {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, + {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, @@ -1547,29 +1564,29 @@ msgpack = [ {file = "msgpack-1.0.2.tar.gz", hash = "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984"}, ] mypy = [ - {file = "mypy-0.902-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243"}, - {file = "mypy-0.902-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8"}, - {file = "mypy-0.902-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4"}, - {file = "mypy-0.902-cp35-cp35m-win_amd64.whl", hash = "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0"}, - {file = "mypy-0.902-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9"}, - {file = "mypy-0.902-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd"}, - {file = "mypy-0.902-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987"}, - {file = "mypy-0.902-cp36-cp36m-win_amd64.whl", hash = "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21"}, - {file = "mypy-0.902-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76"}, - {file = "mypy-0.902-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2"}, - {file = "mypy-0.902-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70"}, - {file = "mypy-0.902-cp37-cp37m-win_amd64.whl", hash = "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4"}, - {file = "mypy-0.902-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1"}, - {file = "mypy-0.902-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116"}, - {file = "mypy-0.902-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8"}, - {file = "mypy-0.902-cp38-cp38-win_amd64.whl", hash = "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167"}, - {file = "mypy-0.902-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da"}, - {file = "mypy-0.902-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2"}, - {file = "mypy-0.902-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20"}, - {file = "mypy-0.902-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb"}, - {file = "mypy-0.902-cp39-cp39-win_amd64.whl", hash = "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab"}, - {file = "mypy-0.902-py3-none-any.whl", hash = "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269"}, - {file = "mypy-0.902.tar.gz", hash = "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c"}, + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1784,6 +1801,10 @@ requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] +single-source = [ + {file = "single-source-0.2.0.tar.gz", hash = "sha256:f40f94c7f2e72c854b9c0c6f7d6b545d74ada9ebd454f9f07f8fb743b22bccf5"}, + {file = "single_source-0.2.0-py3-none-any.whl", hash = "sha256:82c55b00515a30c8f7c262b986a399ca023911ebb6a86f0953d4c50448c36b16"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1848,29 +1869,40 @@ tox = [ {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"}, {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"}, ] +types-attrs = [ + {file = "types_attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:d11acf7a2531a7c52a740c30fa3eb8d01d3066c10d34c01ff5e59502caac5352"}, +] types-cryptography = [ - {file = "types_cryptography-3.3.2-py2.py3-none-any.whl", hash = "sha256:9c0e7018ba376c5161b0f5c4757718045fe536d1f33aed801890613620d03b39"}, + {file = "types-cryptography-3.3.3.tar.gz", hash = "sha256:de2b12e971023b25bd96b7da251dc747ea5b4670a998f9160153d96e4f918f0b"}, + {file = "types_cryptography-3.3.3-py2.py3-none-any.whl", hash = "sha256:524afedb2e7f3fa4b22bd164efb82809a75ff960cc5d40a2fa9b7c2cda228fab"}, ] types-enum34 = [ - {file = "types_enum34-0.1.5-py2.py3-none-any.whl", hash = "sha256:8ea6c9ba83103abd8802f2d459f5b08e59cfcff38ef530c007735296a06a35e3"}, + {file = "types-enum34-0.1.8.tar.gz", hash = "sha256:9e91a94f1f42c73bd7aa43f19a157d722522d29097f301748a034e08693a423d"}, + {file = "types_enum34-0.1.8-py3-none-any.whl", hash = "sha256:5833cfd003d7a860e8442d04a06aaa2bef1f050933f844fa7bd3634914e1aa7f"}, ] types-ipaddress = [ - {file = "types_ipaddress-0.1.2-py2.py3-none-any.whl", hash = "sha256:1074af46f90b81ec2102999d49c557b63fe648df2baba407949f87e7140c7758"}, + {file = "types-ipaddress-0.1.5.tar.gz", hash = "sha256:ffa2c4fe2e8f51898465e2142d4d325fbab009aaa7822db20a4e2e3a896560d7"}, + {file = "types_ipaddress-0.1.5-py3-none-any.whl", hash = "sha256:137ab8a5a895a48157438c8f3eea738e2c22e82c1114ab20ffd78c8637bbc008"}, ] types-paramiko = [ - {file = "types_paramiko-0.1.6-py2.py3-none-any.whl", hash = "sha256:d6dd1aedbea7ed92f05bac751c50817ca5d5d85fbdebbaa34251df87ab67ed7c"}, + {file = "types-paramiko-0.1.7.tar.gz", hash = "sha256:51ce59aea222c47d0590e2b8d8cefbc5aeb469d8bf6368e4f06ad3c61c932c72"}, + {file = "types_paramiko-0.1.7-py2.py3-none-any.whl", hash = "sha256:9fb9f3a363c99ae735148a7ce3c28adbe8f87797ec3c8cce3103899acbdebfa8"}, ] types-pkg-resources = [ - {file = "types_pkg_resources-0.1.2-py2.py3-none-any.whl", hash = "sha256:42d640500de564f1ccc21f918117afadf78039e4fa7f513c647ccf742d609aeb"}, + {file = "types-pkg_resources-0.1.3.tar.gz", hash = "sha256:834a9b8d3dbea343562fd99d5d3359a726f6bf9d3733bccd2b4f3096fbab9dae"}, + {file = "types_pkg_resources-0.1.3-py2.py3-none-any.whl", hash = "sha256:0cb9972cee992249f93fff1a491bf2dc3ce674e5a1926e27d4f0866f7d9b6d9c"}, ] types-requests = [ - {file = "types_requests-0.1.11-py2.py3-none-any.whl", hash = "sha256:e79c09e5866c6520cdf23dc260573a0a190444f36b112116fadc5542656c8309"}, + {file = "types-requests-2.25.0.tar.gz", hash = "sha256:ee0d0c507210141b7d5b8639cc43eaa726084178775db2a5fb06fbf85c185808"}, + {file = "types_requests-2.25.0-py3-none-any.whl", hash = "sha256:fa5c1e5e832ff6193507d8da7e1159281383908ee193a2f4b37bc08140b51844"}, ] types-tabulate = [ - {file = "types_tabulate-0.1.0-py2.py3-none-any.whl", hash = "sha256:756430aa053fb6e72a0e33a74cd451ec71b1dc493ab24b5be8abe34d874d0f13"}, + {file = "types-tabulate-0.1.1.tar.gz", hash = "sha256:d1a415a8601391bc8450a669402ad57a1b620acf8d8d206fcbb3accc1440b106"}, + {file = "types_tabulate-0.1.1-py2.py3-none-any.whl", hash = "sha256:34a4900c32a56db8c7e5a1c8857332e8d02fe45077ea0ed53a3a12a10cbdbd0c"}, ] types-toml = [ - {file = "types_toml-0.1.2-py2.py3-none-any.whl", hash = "sha256:92fe51b57534bc5c16da2d5e2a712aa229c86f8872df95d5f58dcb6977a376ce"}, + {file = "types-toml-0.1.3.tar.gz", hash = "sha256:33ebe67bebaec55a123ecbaa2bd98fe588335d8d8dda2c7ac53502ef5a81a79a"}, + {file = "types_toml-0.1.3-py2.py3-none-any.whl", hash = "sha256:d4add39a90993173d49ff0b069edd122c66ad4cf5c01082b590e380ca670ee1a"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, diff --git a/pyproject.toml b/pyproject.toml index de9990d9..a8c92345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "matrixctl" -version = "0.10.1" +version = "0.10.2" description = "Controls a synapse oci-container instance via ansible" license = "GPL-3.0-or-later" readme = "README.md" @@ -34,6 +34,8 @@ tabulate = "^0.8.9" paramiko = "^2.7.2" toml = "^0.10.2" ansible-runner = "^1.4.7" +attrs = "^21.2.0" +single-source = "^0.2.0" sphinx = { version = ">=3.5.1,<5.0.0", optional = true } sphinx-autodoc-typehints = { version = "^1.12.0", optional = true } sphinxcontrib-programoutput = { version = ">=0.16,<0.18", optional = true } @@ -52,20 +54,21 @@ pylint = "^2.8.3" pycodestyle = "^2.7.0" yapf = "^0.31.0" vulture = "^2.3" -mypy = "^0.902" +mypy = "^0.910" coverage = "^5.5" interrogate = "^1.4.0" towncrier = "^21.3.0" tox = "^3.23.1" # python-language-server = {version = "^0.36.2", extras = ["all"]} black = { version = "^21.6b0", allow-prereleases = true } -isort = {version = "^5.8.0", extras = ["pyproject"]} +isort = {version = "^5.9.1", extras = ["pyproject"]} flake8-bugbear = "^21.4.3" -types-pkg-resources = "^0.1.2" -types-toml = "^0.1.2" -types-requests = "^0.1.11" -types-tabulate = "^0.1.0" -types-paramiko = "^0.1.6" +types-pkg-resources = "^0.1.3" +types-toml = "^0.1.3" +types-requests = "^2.25.0" +types-tabulate = "^0.1.1" +types-paramiko = "^0.1.7" +types-attrs = "^19.1.0" [tool.poetry.scripts] matrixctl = "matrixctl.__main__:main" @@ -122,6 +125,7 @@ known_third_party = [ "sphinx_rtd_theme", "git", "toml", + "attr", "ansible_runner", ] @@ -161,9 +165,7 @@ filename = "CHANGELOG.rst" issue_format = "`#{issue} `_" directory = "news/" top_line = false -# name = "MatrixCtl" title_format = "{version} ({project_date})" # {name} -start_string = ".. towncrier release notes start" all_bullets = true # make all fragments bullet points wrap = true # Wrap text to 79 characters template = "news/templates/default.rst"