fix: format all code with black
and from now on should not deviate from that...
This commit is contained in:
		
							parent
							
								
									18f7fa6c51
								
							
						
					
					
						commit
						8be1b66c9e
					
				
					 10 changed files with 334 additions and 258 deletions
				
			
		
							
								
								
									
										30
									
								
								docs/conf.py
									
										
									
									
									
								
							
							
						
						
									
										30
									
								
								docs/conf.py
									
										
									
									
									
								
							| 
						 | 
					@ -8,33 +8,33 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from importlib.metadata import version as get_version
 | 
					from importlib.metadata import version as get_version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
project = 'WuttaTell'
 | 
					project = "WuttaTell"
 | 
				
			||||||
copyright = '2025, Lance Edgar'
 | 
					copyright = "2025, Lance Edgar"
 | 
				
			||||||
author = 'Lance Edgar'
 | 
					author = "Lance Edgar"
 | 
				
			||||||
release = get_version('WuttaTell')
 | 
					release = get_version("WuttaTell")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- General configuration ---------------------------------------------------
 | 
					# -- General configuration ---------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
extensions = [
 | 
					extensions = [
 | 
				
			||||||
    'sphinx.ext.autodoc',
 | 
					    "sphinx.ext.autodoc",
 | 
				
			||||||
    'sphinx.ext.intersphinx',
 | 
					    "sphinx.ext.intersphinx",
 | 
				
			||||||
    'sphinx.ext.viewcode',
 | 
					    "sphinx.ext.viewcode",
 | 
				
			||||||
    'sphinx.ext.todo',
 | 
					    "sphinx.ext.todo",
 | 
				
			||||||
    'sphinxcontrib.programoutput',
 | 
					    "sphinxcontrib.programoutput",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
templates_path = ['_templates']
 | 
					templates_path = ["_templates"]
 | 
				
			||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
 | 
					exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
intersphinx_mapping = {
 | 
					intersphinx_mapping = {
 | 
				
			||||||
    'requests': ('https://requests.readthedocs.io/en/latest/', None),
 | 
					    "requests": ("https://requests.readthedocs.io/en/latest/", None),
 | 
				
			||||||
    'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
 | 
					    "wuttjamaican": ("https://docs.wuttaproject.org/wuttjamaican/", None),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- Options for HTML output -------------------------------------------------
 | 
					# -- Options for HTML output -------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html_theme = 'furo'
 | 
					html_theme = "furo"
 | 
				
			||||||
html_static_path = ['_static']
 | 
					html_static_path = ["_static"]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,4 +3,4 @@
 | 
				
			||||||
from importlib.metadata import version
 | 
					from importlib.metadata import version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = version('WuttaTell')
 | 
					__version__ = version("WuttaTell")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,9 +41,11 @@ class WuttaTellAppProvider(AppProvider):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :rtype: :class:`~wuttatell.telemetry.TelemetryHandler`
 | 
					        :rtype: :class:`~wuttatell.telemetry.TelemetryHandler`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, 'telemetry_handler'):
 | 
					        if not hasattr(self, "telemetry_handler"):
 | 
				
			||||||
            spec = self.config.get(f'{self.appname}.telemetry.handler',
 | 
					            spec = self.config.get(
 | 
				
			||||||
                                   default='wuttatell.telemetry:TelemetryHandler')
 | 
					                f"{self.appname}.telemetry.handler",
 | 
				
			||||||
 | 
					                default="wuttatell.telemetry:TelemetryHandler",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            factory = self.app.load_object(spec)
 | 
					            factory = self.app.load_object(spec)
 | 
				
			||||||
            self.telemetry_handler = factory(self.config, **kwargs)
 | 
					            self.telemetry_handler = factory(self.config, **kwargs)
 | 
				
			||||||
        return self.telemetry_handler
 | 
					        return self.telemetry_handler
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,18 +37,24 @@ log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@wutta_typer.command()
 | 
					@wutta_typer.command()
 | 
				
			||||||
def tell(
 | 
					def tell(
 | 
				
			||||||
        ctx: typer.Context,
 | 
					    ctx: typer.Context,
 | 
				
			||||||
        profile: Annotated[
 | 
					    profile: Annotated[
 | 
				
			||||||
            str,
 | 
					        str,
 | 
				
			||||||
            typer.Option('--profile', '-p',
 | 
					        typer.Option(
 | 
				
			||||||
                         help="Profile (type) of telemetry data to collect.  "
 | 
					            "--profile",
 | 
				
			||||||
                         "This also determines where/how data is submitted.  "
 | 
					            "-p",
 | 
				
			||||||
                         "If not specified, default profile is assumed.")] = None,
 | 
					            help="Profile (type) of telemetry data to collect.  "
 | 
				
			||||||
        dry_run: Annotated[
 | 
					            "This also determines where/how data is submitted.  "
 | 
				
			||||||
            bool,
 | 
					            "If not specified, default profile is assumed.",
 | 
				
			||||||
            typer.Option('--dry-run',
 | 
					        ),
 | 
				
			||||||
                         help="Go through all the motions but do not submit "
 | 
					    ] = None,
 | 
				
			||||||
                         "the data to server.")] = False,
 | 
					    dry_run: Annotated[
 | 
				
			||||||
 | 
					        bool,
 | 
				
			||||||
 | 
					        typer.Option(
 | 
				
			||||||
 | 
					            "--dry-run",
 | 
				
			||||||
 | 
					            help="Go through all the motions but do not submit " "the data to server.",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ] = False,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Collect and submit telemetry data
 | 
					    Collect and submit telemetry data
 | 
				
			||||||
| 
						 | 
					@ -58,7 +64,7 @@ def tell(
 | 
				
			||||||
    telemetry = app.get_telemetry_handler()
 | 
					    telemetry = app.get_telemetry_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    data = telemetry.collect_all_data(profile=profile)
 | 
					    data = telemetry.collect_all_data(profile=profile)
 | 
				
			||||||
    log.info("data collected for: %s", ', '.join(sorted(data)))
 | 
					    log.info("data collected for: %s", ", ".join(sorted(data)))
 | 
				
			||||||
    log.debug("%s", data)
 | 
					    log.debug("%s", data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if dry_run:
 | 
					    if dry_run:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,23 +71,30 @@ class SimpleAPIClient:
 | 
				
			||||||
       API requests.
 | 
					       API requests.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, config, base_url=None, token=None, ssl_verify=None, max_retries=None):
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self, config, base_url=None, token=None, ssl_verify=None, max_retries=None
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        self.config = config
 | 
					        self.config = config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.base_url = base_url or self.config.require(f'{self.config.appname}.api.base_url')
 | 
					        self.base_url = base_url or self.config.require(
 | 
				
			||||||
        self.base_url = self.base_url.rstrip('/')
 | 
					            f"{self.config.appname}.api.base_url"
 | 
				
			||||||
        self.token = token or self.config.require(f'{self.config.appname}.api.token')
 | 
					        )
 | 
				
			||||||
 | 
					        self.base_url = self.base_url.rstrip("/")
 | 
				
			||||||
 | 
					        self.token = token or self.config.require(f"{self.config.appname}.api.token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if max_retries is not None:
 | 
					        if max_retries is not None:
 | 
				
			||||||
            self.max_retries = max_retries
 | 
					            self.max_retries = max_retries
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.max_retries = self.config.get_int(f'{self.config.appname}.api.max_retries')
 | 
					            self.max_retries = self.config.get_int(
 | 
				
			||||||
 | 
					                f"{self.config.appname}.api.max_retries"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ssl_verify is not None:
 | 
					        if ssl_verify is not None:
 | 
				
			||||||
            self.ssl_verify = ssl_verify
 | 
					            self.ssl_verify = ssl_verify
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.ssl_verify = self.config.get_bool(f'{self.config.appname}.api.ssl_verify',
 | 
					            self.ssl_verify = self.config.get_bool(
 | 
				
			||||||
                                                   default=True)
 | 
					                f"{self.config.appname}.api.ssl_verify", default=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.session = None
 | 
					        self.session = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,14 +129,18 @@ class SimpleAPIClient:
 | 
				
			||||||
        # without it, can get error response:
 | 
					        # without it, can get error response:
 | 
				
			||||||
        # 400 Client Error: Bad CSRF Origin for url
 | 
					        # 400 Client Error: Bad CSRF Origin for url
 | 
				
			||||||
        parts = urlparse(self.base_url)
 | 
					        parts = urlparse(self.base_url)
 | 
				
			||||||
        self.session.headers.update({
 | 
					        self.session.headers.update(
 | 
				
			||||||
            'Origin': f'{parts.scheme}://{parts.netloc}',
 | 
					            {
 | 
				
			||||||
        })
 | 
					                "Origin": f"{parts.scheme}://{parts.netloc}",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # authenticate via token only (for now?)
 | 
					        # authenticate via token only (for now?)
 | 
				
			||||||
        self.session.headers.update({
 | 
					        self.session.headers.update(
 | 
				
			||||||
            'Authorization': f'Bearer {self.token}',
 | 
					            {
 | 
				
			||||||
        })
 | 
					                "Authorization": f"Bearer {self.token}",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_request(self, request_method, api_method, params=None, data=None):
 | 
					    def make_request(self, request_method, api_method, params=None, data=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -153,13 +164,12 @@ class SimpleAPIClient:
 | 
				
			||||||
        :rtype: :class:`requests:requests.Response` instance.
 | 
					        :rtype: :class:`requests:requests.Response` instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.init_session()
 | 
					        self.init_session()
 | 
				
			||||||
        api_method = api_method.lstrip('/')
 | 
					        api_method = api_method.lstrip("/")
 | 
				
			||||||
        url = f'{self.base_url}/{api_method}'
 | 
					        url = f"{self.base_url}/{api_method}"
 | 
				
			||||||
        if request_method == 'GET':
 | 
					        if request_method == "GET":
 | 
				
			||||||
            response = self.session.get(url, params=params)
 | 
					            response = self.session.get(url, params=params)
 | 
				
			||||||
        elif request_method == 'POST':
 | 
					        elif request_method == "POST":
 | 
				
			||||||
            response = self.session.post(url, params=params,
 | 
					            response = self.session.post(url, params=params, data=json.dumps(data))
 | 
				
			||||||
                                         data=json.dumps(data))
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            raise NotImplementedError(f"unsupported request method: {request_method}")
 | 
					            raise NotImplementedError(f"unsupported request method: {request_method}")
 | 
				
			||||||
        response.raise_for_status()
 | 
					        response.raise_for_status()
 | 
				
			||||||
| 
						 | 
					@ -180,7 +190,7 @@ class SimpleAPIClient:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :rtype: :class:`requests:requests.Response` instance.
 | 
					        :rtype: :class:`requests:requests.Response` instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.make_request('GET', api_method, params=params)
 | 
					        return self.make_request("GET", api_method, params=params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, api_method, **kwargs):
 | 
					    def post(self, api_method, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -194,4 +204,4 @@ class SimpleAPIClient:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :rtype: :class:`requests:requests.Response` instance.
 | 
					        :rtype: :class:`requests:requests.Response` instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.make_request('POST', api_method, **kwargs)
 | 
					        return self.make_request("POST", api_method, **kwargs)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ class TelemetryHandler(GenericHandler):
 | 
				
			||||||
        if isinstance(profile, TelemetryProfile):
 | 
					        if isinstance(profile, TelemetryProfile):
 | 
				
			||||||
            return profile
 | 
					            return profile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return TelemetryProfile(self.config, profile or 'default')
 | 
					        return TelemetryProfile(self.config, profile or "default")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def collect_all_data(self, profile=None):
 | 
					    def collect_all_data(self, profile=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -76,7 +76,7 @@ class TelemetryHandler(GenericHandler):
 | 
				
			||||||
        profile = self.get_profile(profile)
 | 
					        profile = self.get_profile(profile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for key in profile.collect_keys:
 | 
					        for key in profile.collect_keys:
 | 
				
			||||||
            collector = getattr(self, f'collect_data_{key}')
 | 
					            collector = getattr(self, f"collect_data_{key}")
 | 
				
			||||||
            data[key] = collector(profile=profile)
 | 
					            data[key] = collector(profile=profile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.normalize_errors(data)
 | 
					        self.normalize_errors(data)
 | 
				
			||||||
| 
						 | 
					@ -87,11 +87,11 @@ class TelemetryHandler(GenericHandler):
 | 
				
			||||||
        all_errors = []
 | 
					        all_errors = []
 | 
				
			||||||
        for key, value in data.items():
 | 
					        for key, value in data.items():
 | 
				
			||||||
            if value:
 | 
					            if value:
 | 
				
			||||||
                errors = value.pop('errors', None)
 | 
					                errors = value.pop("errors", None)
 | 
				
			||||||
                if errors:
 | 
					                if errors:
 | 
				
			||||||
                    all_errors.extend(errors)
 | 
					                    all_errors.extend(errors)
 | 
				
			||||||
        if all_errors:
 | 
					        if all_errors:
 | 
				
			||||||
            data['errors'] = all_errors
 | 
					            data["errors"] = all_errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def collect_data_os(self, profile, **kwargs):
 | 
					    def collect_data_os(self, profile, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -119,40 +119,40 @@ class TelemetryHandler(GenericHandler):
 | 
				
			||||||
        errors = []
 | 
					        errors = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # release
 | 
					        # release
 | 
				
			||||||
        release_path = kwargs.get('release_path', '/etc/os-release')
 | 
					        release_path = kwargs.get("release_path", "/etc/os-release")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            with open(release_path, 'rt') as f:
 | 
					            with open(release_path, "rt") as f:
 | 
				
			||||||
                output = f.read()
 | 
					                output = f.read()
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            errors.append(f"Failed to read {release_path}")
 | 
					            errors.append(f"Failed to read {release_path}")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            release = {}
 | 
					            release = {}
 | 
				
			||||||
            pattern = re.compile(r'^([^=]+)=(.*)$')
 | 
					            pattern = re.compile(r"^([^=]+)=(.*)$")
 | 
				
			||||||
            for line in output.strip().split('\n'):
 | 
					            for line in output.strip().split("\n"):
 | 
				
			||||||
                if match := pattern.match(line):
 | 
					                if match := pattern.match(line):
 | 
				
			||||||
                    key, val = match.groups()
 | 
					                    key, val = match.groups()
 | 
				
			||||||
                    if val.startswith('"') and val.endswith('"'):
 | 
					                    if val.startswith('"') and val.endswith('"'):
 | 
				
			||||||
                        val = val.strip('"')
 | 
					                        val = val.strip('"')
 | 
				
			||||||
                    release[key] = val
 | 
					                    release[key] = val
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                data['release_id'] = release['ID']
 | 
					                data["release_id"] = release["ID"]
 | 
				
			||||||
                data['release_version'] = release['VERSION_ID']
 | 
					                data["release_version"] = release["VERSION_ID"]
 | 
				
			||||||
                data['release_full'] = release['PRETTY_NAME']
 | 
					                data["release_full"] = release["PRETTY_NAME"]
 | 
				
			||||||
            except KeyError:
 | 
					            except KeyError:
 | 
				
			||||||
                errors.append(f"Failed to parse {release_path}")
 | 
					                errors.append(f"Failed to parse {release_path}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # timezone
 | 
					        # timezone
 | 
				
			||||||
        timezone_path = kwargs.get('timezone_path', '/etc/timezone')
 | 
					        timezone_path = kwargs.get("timezone_path", "/etc/timezone")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            with open(timezone_path, 'rt') as f:
 | 
					            with open(timezone_path, "rt") as f:
 | 
				
			||||||
                output = f.read()
 | 
					                output = f.read()
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            errors.append(f"Failed to read {timezone_path}")
 | 
					            errors.append(f"Failed to read {timezone_path}")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            data['timezone'] = output.strip()
 | 
					            data["timezone"] = output.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if errors:
 | 
					        if errors:
 | 
				
			||||||
            data['errors'] = errors
 | 
					            data["errors"] = errors
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def collect_data_python(self, profile):
 | 
					    def collect_data_python(self, profile):
 | 
				
			||||||
| 
						 | 
					@ -191,31 +191,32 @@ class TelemetryHandler(GenericHandler):
 | 
				
			||||||
        errors = []
 | 
					        errors = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # envroot determines python executable
 | 
					        # envroot determines python executable
 | 
				
			||||||
        envroot = profile.get_str('collect.python.envroot')
 | 
					        envroot = profile.get_str("collect.python.envroot")
 | 
				
			||||||
        if envroot:
 | 
					        if envroot:
 | 
				
			||||||
            data['envroot'] = envroot
 | 
					            data["envroot"] = envroot
 | 
				
			||||||
            python = os.path.join(envroot, 'bin/python')
 | 
					            python = os.path.join(envroot, "bin/python")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            python = profile.get_str('collect.python.executable',
 | 
					            python = profile.get_str(
 | 
				
			||||||
                                     default='/usr/bin/python3')
 | 
					                "collect.python.executable", default="/usr/bin/python3"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # python version
 | 
					        # python version
 | 
				
			||||||
        data['executable'] = python
 | 
					        data["executable"] = python
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            output = subprocess.check_output([python, '--version'])
 | 
					            output = subprocess.check_output([python, "--version"])
 | 
				
			||||||
        except (subprocess.CalledProcessError, FileNotFoundError) as err:
 | 
					        except (subprocess.CalledProcessError, FileNotFoundError) as err:
 | 
				
			||||||
            errors.append("Failed to execute `python --version`")
 | 
					            errors.append("Failed to execute `python --version`")
 | 
				
			||||||
            errors.append(str(err))
 | 
					            errors.append(str(err))
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            output = output.decode('utf_8').strip()
 | 
					            output = output.decode("utf_8").strip()
 | 
				
			||||||
            data['release_full'] = output
 | 
					            data["release_full"] = output
 | 
				
			||||||
            if match := re.match(r'^Python (\d+\.\d+\.\d+)', output):
 | 
					            if match := re.match(r"^Python (\d+\.\d+\.\d+)", output):
 | 
				
			||||||
                data['release_version'] = match.group(1)
 | 
					                data["release_version"] = match.group(1)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                errors.append("Failed to parse Python version")
 | 
					                errors.append("Failed to parse Python version")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if errors:
 | 
					        if errors:
 | 
				
			||||||
            data['errors'] = errors
 | 
					            data["errors"] = errors
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def submit_all_data(self, profile=None, data=None):
 | 
					    def submit_all_data(self, profile=None, data=None):
 | 
				
			||||||
| 
						 | 
					@ -269,6 +270,6 @@ class TelemetryProfile(WuttaConfigProfile):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def load(self):
 | 
					    def load(self):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        keys = self.get_str('collect.keys', default='os,python')
 | 
					        keys = self.get_str("collect.keys", default="os,python")
 | 
				
			||||||
        self.collect_keys = self.config.parse_list(keys)
 | 
					        self.collect_keys = self.config.parse_list(keys)
 | 
				
			||||||
        self.submit_url = self.get_str('submit.url')
 | 
					        self.submit_url = self.get_str("submit.url")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								tasks.py
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								tasks.py
									
										
									
									
									
								
							| 
						 | 
					@ -15,10 +15,10 @@ def release(c, skip_tests=False):
 | 
				
			||||||
    Release a new version of WuttaTell
 | 
					    Release a new version of WuttaTell
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if not skip_tests:
 | 
					    if not skip_tests:
 | 
				
			||||||
        c.run('pytest')
 | 
					        c.run("pytest")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if os.path.exists('dist'):
 | 
					    if os.path.exists("dist"):
 | 
				
			||||||
        shutil.rmtree('dist')
 | 
					        shutil.rmtree("dist")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    c.run('python -m build --sdist')
 | 
					    c.run("python -m build --sdist")
 | 
				
			||||||
    c.run('twine upload dist/*')
 | 
					    c.run("twine upload dist/*")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,10 +14,10 @@ class TestTell(ConfigTestCase):
 | 
				
			||||||
        ctx = Mock()
 | 
					        ctx = Mock()
 | 
				
			||||||
        ctx.parent = Mock()
 | 
					        ctx.parent = Mock()
 | 
				
			||||||
        ctx.parent.wutta_config = self.config
 | 
					        ctx.parent.wutta_config = self.config
 | 
				
			||||||
        with patch.object(TelemetryHandler, 'submit_all_data') as submit_all_data:
 | 
					        with patch.object(TelemetryHandler, "submit_all_data") as submit_all_data:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # dry run
 | 
					            # dry run
 | 
				
			||||||
            with patch.object(TelemetryHandler, 'collect_all_data') as collect_all_data:
 | 
					            with patch.object(TelemetryHandler, "collect_all_data") as collect_all_data:
 | 
				
			||||||
                mod.tell(ctx, dry_run=True)
 | 
					                mod.tell(ctx, dry_run=True)
 | 
				
			||||||
                collect_all_data.assert_called_once_with(profile=None)
 | 
					                collect_all_data.assert_called_once_with(profile=None)
 | 
				
			||||||
                submit_all_data.assert_not_called()
 | 
					                submit_all_data.assert_not_called()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,33 +21,42 @@ class TestSimpleAPIClient(ConfigTestCase):
 | 
				
			||||||
    def test_constructor(self):
 | 
					    def test_constructor(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # caller specifies params
 | 
					        # caller specifies params
 | 
				
			||||||
        client = self.make_client(base_url='https://example.com/api/',
 | 
					        client = self.make_client(
 | 
				
			||||||
                                  token='XYZPDQ12345',
 | 
					            base_url="https://example.com/api/",
 | 
				
			||||||
                                  ssl_verify=False,
 | 
					            token="XYZPDQ12345",
 | 
				
			||||||
                                  max_retries=5)
 | 
					            ssl_verify=False,
 | 
				
			||||||
        self.assertEqual(client.base_url, 'https://example.com/api') # no trailing slash
 | 
					            max_retries=5,
 | 
				
			||||||
        self.assertEqual(client.token, 'XYZPDQ12345')
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            client.base_url, "https://example.com/api"
 | 
				
			||||||
 | 
					        )  # no trailing slash
 | 
				
			||||||
 | 
					        self.assertEqual(client.token, "XYZPDQ12345")
 | 
				
			||||||
        self.assertFalse(client.ssl_verify)
 | 
					        self.assertFalse(client.ssl_verify)
 | 
				
			||||||
        self.assertEqual(client.max_retries, 5)
 | 
					        self.assertEqual(client.max_retries, 5)
 | 
				
			||||||
        self.assertIsNone(client.session)
 | 
					        self.assertIsNone(client.session)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # now with some defaults
 | 
					        # now with some defaults
 | 
				
			||||||
        client = self.make_client(base_url='https://example.com/api/',
 | 
					        client = self.make_client(
 | 
				
			||||||
                                  token='XYZPDQ12345')
 | 
					            base_url="https://example.com/api/", token="XYZPDQ12345"
 | 
				
			||||||
        self.assertEqual(client.base_url, 'https://example.com/api') # no trailing slash
 | 
					        )
 | 
				
			||||||
        self.assertEqual(client.token, 'XYZPDQ12345')
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            client.base_url, "https://example.com/api"
 | 
				
			||||||
 | 
					        )  # no trailing slash
 | 
				
			||||||
 | 
					        self.assertEqual(client.token, "XYZPDQ12345")
 | 
				
			||||||
        self.assertTrue(client.ssl_verify)
 | 
					        self.assertTrue(client.ssl_verify)
 | 
				
			||||||
        self.assertIsNone(client.max_retries)
 | 
					        self.assertIsNone(client.max_retries)
 | 
				
			||||||
        self.assertIsNone(client.session)
 | 
					        self.assertIsNone(client.session)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # now from config
 | 
					        # now from config
 | 
				
			||||||
        self.config.setdefault('wutta.api.base_url', 'https://another.com/api/')
 | 
					        self.config.setdefault("wutta.api.base_url", "https://another.com/api/")
 | 
				
			||||||
        self.config.setdefault('wutta.api.token', '9843243q4')
 | 
					        self.config.setdefault("wutta.api.token", "9843243q4")
 | 
				
			||||||
        self.config.setdefault('wutta.api.ssl_verify', 'false')
 | 
					        self.config.setdefault("wutta.api.ssl_verify", "false")
 | 
				
			||||||
        self.config.setdefault('wutta.api.max_retries', '4')
 | 
					        self.config.setdefault("wutta.api.max_retries", "4")
 | 
				
			||||||
        client = self.make_client()
 | 
					        client = self.make_client()
 | 
				
			||||||
        self.assertEqual(client.base_url, 'https://another.com/api') # no trailing slash
 | 
					        self.assertEqual(
 | 
				
			||||||
        self.assertEqual(client.token, '9843243q4')
 | 
					            client.base_url, "https://another.com/api"
 | 
				
			||||||
 | 
					        )  # no trailing slash
 | 
				
			||||||
 | 
					        self.assertEqual(client.token, "9843243q4")
 | 
				
			||||||
        self.assertFalse(client.ssl_verify)
 | 
					        self.assertFalse(client.ssl_verify)
 | 
				
			||||||
        self.assertEqual(client.max_retries, 4)
 | 
					        self.assertEqual(client.max_retries, 4)
 | 
				
			||||||
        self.assertIsNone(client.session)
 | 
					        self.assertIsNone(client.session)
 | 
				
			||||||
| 
						 | 
					@ -55,16 +64,18 @@ class TestSimpleAPIClient(ConfigTestCase):
 | 
				
			||||||
    def test_init_session(self):
 | 
					    def test_init_session(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # client begins with no session
 | 
					        # client begins with no session
 | 
				
			||||||
        client = self.make_client(base_url='https://example.com/api', token='1234')
 | 
					        client = self.make_client(base_url="https://example.com/api", token="1234")
 | 
				
			||||||
        self.assertIsNone(client.session)
 | 
					        self.assertIsNone(client.session)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # session is created here
 | 
					        # session is created here
 | 
				
			||||||
        client.init_session()
 | 
					        client.init_session()
 | 
				
			||||||
        self.assertIsInstance(client.session, requests.Session)
 | 
					        self.assertIsInstance(client.session, requests.Session)
 | 
				
			||||||
        self.assertTrue(client.session.verify)
 | 
					        self.assertTrue(client.session.verify)
 | 
				
			||||||
        self.assertTrue(all([a.max_retries.total == 0 for a in client.session.adapters.values()]))
 | 
					        self.assertTrue(
 | 
				
			||||||
        self.assertIn('Authorization', client.session.headers)
 | 
					            all([a.max_retries.total == 0 for a in client.session.adapters.values()])
 | 
				
			||||||
        self.assertEqual(client.session.headers['Authorization'], 'Bearer 1234')
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertIn("Authorization", client.session.headers)
 | 
				
			||||||
 | 
					        self.assertEqual(client.session.headers["Authorization"], "Bearer 1234")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # session is never re-created
 | 
					        # session is never re-created
 | 
				
			||||||
        orig_session = client.session
 | 
					        orig_session = client.session
 | 
				
			||||||
| 
						 | 
					@ -72,94 +83,124 @@ class TestSimpleAPIClient(ConfigTestCase):
 | 
				
			||||||
        self.assertIs(client.session, orig_session)
 | 
					        self.assertIs(client.session, orig_session)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # new client/session with no ssl_verify
 | 
					        # new client/session with no ssl_verify
 | 
				
			||||||
        client = self.make_client(base_url='https://example.com/api', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
 | 
					            base_url="https://example.com/api", token="1234", ssl_verify=False
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        client.init_session()
 | 
					        client.init_session()
 | 
				
			||||||
        self.assertFalse(client.session.verify)
 | 
					        self.assertFalse(client.session.verify)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # new client/session with max_retries
 | 
					        # new client/session with max_retries
 | 
				
			||||||
        client = self.make_client(base_url='https://example.com/api', token='1234', max_retries=5)
 | 
					        client = self.make_client(
 | 
				
			||||||
 | 
					            base_url="https://example.com/api", token="1234", max_retries=5
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        client.init_session()
 | 
					        client.init_session()
 | 
				
			||||||
        self.assertEqual(client.session.adapters['https://example.com/api'].max_retries.total, 5)
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            client.session.adapters["https://example.com/api"].max_retries.total, 5
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_make_request_get(self):
 | 
					    def test_make_request_get(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # start server
 | 
					        # start server
 | 
				
			||||||
        threading.Thread(target=start_server).start()
 | 
					        threading.Thread(target=start_server).start()
 | 
				
			||||||
        while not SERVER['running']:
 | 
					        while not SERVER["running"]:
 | 
				
			||||||
            time.sleep(0.02)
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # server returns our headers
 | 
					        # server returns our headers
 | 
				
			||||||
        client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
        response = client.make_request('GET', '/telemetry')
 | 
					            base_url=f'http://127.0.0.1:{SERVER["port"]}',
 | 
				
			||||||
 | 
					            token="1234",
 | 
				
			||||||
 | 
					            ssl_verify=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = client.make_request("GET", "/telemetry")
 | 
				
			||||||
        result = response.json()
 | 
					        result = response.json()
 | 
				
			||||||
        self.assertIn('headers', result)
 | 
					        self.assertIn("headers", result)
 | 
				
			||||||
        self.assertIn('Authorization', result['headers'])
 | 
					        self.assertIn("Authorization", result["headers"])
 | 
				
			||||||
        self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
 | 
					        self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
 | 
				
			||||||
        self.assertNotIn('payload', result)
 | 
					        self.assertNotIn("payload", result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_make_request_post(self):
 | 
					    def test_make_request_post(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # start server
 | 
					        # start server
 | 
				
			||||||
        threading.Thread(target=start_server).start()
 | 
					        threading.Thread(target=start_server).start()
 | 
				
			||||||
        while not SERVER['running']:
 | 
					        while not SERVER["running"]:
 | 
				
			||||||
            time.sleep(0.02)
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # server returns our headers + payload
 | 
					        # server returns our headers + payload
 | 
				
			||||||
        client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
        response = client.make_request('POST', '/telemetry', data={'os': {'name': 'debian'}})
 | 
					            base_url=f'http://127.0.0.1:{SERVER["port"]}',
 | 
				
			||||||
 | 
					            token="1234",
 | 
				
			||||||
 | 
					            ssl_verify=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = client.make_request(
 | 
				
			||||||
 | 
					            "POST", "/telemetry", data={"os": {"name": "debian"}}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        result = response.json()
 | 
					        result = response.json()
 | 
				
			||||||
        self.assertIn('headers', result)
 | 
					        self.assertIn("headers", result)
 | 
				
			||||||
        self.assertIn('Authorization', result['headers'])
 | 
					        self.assertIn("Authorization", result["headers"])
 | 
				
			||||||
        self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
 | 
					        self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
 | 
				
			||||||
        self.assertIn('payload', result)
 | 
					        self.assertIn("payload", result)
 | 
				
			||||||
        self.assertEqual(json.loads(result['payload']), {'os': {'name': 'debian'}})
 | 
					        self.assertEqual(json.loads(result["payload"]), {"os": {"name": "debian"}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_make_request_unsupported(self):
 | 
					    def test_make_request_unsupported(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # start server
 | 
					        # start server
 | 
				
			||||||
        threading.Thread(target=start_server).start()
 | 
					        threading.Thread(target=start_server).start()
 | 
				
			||||||
        while not SERVER['running']:
 | 
					        while not SERVER["running"]:
 | 
				
			||||||
            time.sleep(0.02)
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # e.g. DELETE is not implemented
 | 
					        # e.g. DELETE is not implemented
 | 
				
			||||||
        client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
        self.assertRaises(NotImplementedError, client.make_request, 'DELETE', '/telemetry')
 | 
					            base_url=f'http://127.0.0.1:{SERVER["port"]}',
 | 
				
			||||||
 | 
					            token="1234",
 | 
				
			||||||
 | 
					            ssl_verify=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertRaises(
 | 
				
			||||||
 | 
					            NotImplementedError, client.make_request, "DELETE", "/telemetry"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # nb. issue valid request to stop the server
 | 
					        # nb. issue valid request to stop the server
 | 
				
			||||||
        client.make_request('GET', '/telemetry')
 | 
					        client.make_request("GET", "/telemetry")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get(self):
 | 
					    def test_get(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # start server
 | 
					        # start server
 | 
				
			||||||
        threading.Thread(target=start_server).start()
 | 
					        threading.Thread(target=start_server).start()
 | 
				
			||||||
        while not SERVER['running']:
 | 
					        while not SERVER["running"]:
 | 
				
			||||||
            time.sleep(0.02)
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # server returns our headers
 | 
					        # server returns our headers
 | 
				
			||||||
        client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
        response = client.get('/telemetry')
 | 
					            base_url=f'http://127.0.0.1:{SERVER["port"]}',
 | 
				
			||||||
 | 
					            token="1234",
 | 
				
			||||||
 | 
					            ssl_verify=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = client.get("/telemetry")
 | 
				
			||||||
        result = response.json()
 | 
					        result = response.json()
 | 
				
			||||||
        self.assertIn('headers', result)
 | 
					        self.assertIn("headers", result)
 | 
				
			||||||
        self.assertIn('Authorization', result['headers'])
 | 
					        self.assertIn("Authorization", result["headers"])
 | 
				
			||||||
        self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
 | 
					        self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
 | 
				
			||||||
        self.assertNotIn('payload', result)
 | 
					        self.assertNotIn("payload", result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_post(self):
 | 
					    def test_post(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # start server
 | 
					        # start server
 | 
				
			||||||
        threading.Thread(target=start_server).start()
 | 
					        threading.Thread(target=start_server).start()
 | 
				
			||||||
        while not SERVER['running']:
 | 
					        while not SERVER["running"]:
 | 
				
			||||||
            time.sleep(0.02)
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # server returns our headers + payload
 | 
					        # server returns our headers + payload
 | 
				
			||||||
        client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
 | 
					        client = self.make_client(
 | 
				
			||||||
        response = client.post('/telemetry', data={'os': {'name': 'debian'}})
 | 
					            base_url=f'http://127.0.0.1:{SERVER["port"]}',
 | 
				
			||||||
 | 
					            token="1234",
 | 
				
			||||||
 | 
					            ssl_verify=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = client.post("/telemetry", data={"os": {"name": "debian"}})
 | 
				
			||||||
        result = response.json()
 | 
					        result = response.json()
 | 
				
			||||||
        self.assertIn('headers', result)
 | 
					        self.assertIn("headers", result)
 | 
				
			||||||
        self.assertIn('Authorization', result['headers'])
 | 
					        self.assertIn("Authorization", result["headers"])
 | 
				
			||||||
        self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
 | 
					        self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
 | 
				
			||||||
        self.assertIn('payload', result)
 | 
					        self.assertIn("payload", result)
 | 
				
			||||||
        self.assertEqual(json.loads(result['payload']), {'os': {'name': 'debian'}})
 | 
					        self.assertEqual(json.loads(result["payload"]), {"os": {"name": "debian"}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FakeRequestHandler(BaseHTTPRequestHandler):
 | 
					class FakeRequestHandler(BaseHTTPRequestHandler):
 | 
				
			||||||
| 
						 | 
					@ -167,37 +208,38 @@ class FakeRequestHandler(BaseHTTPRequestHandler):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def do_GET(self):
 | 
					    def do_GET(self):
 | 
				
			||||||
        headers = dict([(k, v) for k, v in self.headers.items()])
 | 
					        headers = dict([(k, v) for k, v in self.headers.items()])
 | 
				
			||||||
        result = {'headers': headers}
 | 
					        result = {"headers": headers}
 | 
				
			||||||
        result = json.dumps(result).encode('utf_8')
 | 
					        result = json.dumps(result).encode("utf_8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.send_response(HTTPStatus.OK)
 | 
					        self.send_response(HTTPStatus.OK)
 | 
				
			||||||
        self.send_header("Content-Type", 'text/json')
 | 
					        self.send_header("Content-Type", "text/json")
 | 
				
			||||||
        self.send_header("Content-Length", str(len(result)))
 | 
					        self.send_header("Content-Length", str(len(result)))
 | 
				
			||||||
        self.end_headers()
 | 
					        self.end_headers()
 | 
				
			||||||
        self.wfile.write(result)
 | 
					        self.wfile.write(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def do_POST(self):
 | 
					    def do_POST(self):
 | 
				
			||||||
        headers = dict([(k, v) for k, v in self.headers.items()])
 | 
					        headers = dict([(k, v) for k, v in self.headers.items()])
 | 
				
			||||||
        length = int(self.headers.get('Content-Length'))
 | 
					        length = int(self.headers.get("Content-Length"))
 | 
				
			||||||
        payload = self.rfile.read(length).decode('utf_8')
 | 
					        payload = self.rfile.read(length).decode("utf_8")
 | 
				
			||||||
        result = {'headers': headers, 'payload': payload}
 | 
					        result = {"headers": headers, "payload": payload}
 | 
				
			||||||
        result = json.dumps(result).encode('utf_8')
 | 
					        result = json.dumps(result).encode("utf_8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.send_response(HTTPStatus.OK)
 | 
					        self.send_response(HTTPStatus.OK)
 | 
				
			||||||
        self.send_header("Content-Type", 'text/json')
 | 
					        self.send_header("Content-Type", "text/json")
 | 
				
			||||||
        self.send_header("Content-Length", str(len(result)))
 | 
					        self.send_header("Content-Length", str(len(result)))
 | 
				
			||||||
        self.end_headers()
 | 
					        self.end_headers()
 | 
				
			||||||
        self.wfile.write(result)
 | 
					        self.wfile.write(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SERVER = {'running': False, 'port': 7314}
 | 
					SERVER = {"running": False, "port": 7314}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def start_server():
 | 
					def start_server():
 | 
				
			||||||
    if SERVER['running']:
 | 
					    if SERVER["running"]:
 | 
				
			||||||
        raise RuntimeError("http server is already running")
 | 
					        raise RuntimeError("http server is already running")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with HTTPServer(('127.0.0.1', SERVER['port']), FakeRequestHandler) as httpd:
 | 
					    with HTTPServer(("127.0.0.1", SERVER["port"]), FakeRequestHandler) as httpd:
 | 
				
			||||||
        SERVER['running'] = True
 | 
					        SERVER["running"] = True
 | 
				
			||||||
        httpd.handle_request()
 | 
					        httpd.handle_request()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SERVER['running'] = False
 | 
					    SERVER["running"] = False
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,193 +19,208 @@ class TestTelemetryHandler(ConfigTestCase):
 | 
				
			||||||
    def test_get_profile(self):
 | 
					    def test_get_profile(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # default
 | 
					        # default
 | 
				
			||||||
        default = self.handler.get_profile('default')
 | 
					        default = self.handler.get_profile("default")
 | 
				
			||||||
        self.assertIsInstance(default, mod.TelemetryProfile)
 | 
					        self.assertIsInstance(default, mod.TelemetryProfile)
 | 
				
			||||||
        self.assertEqual(default.key, 'default')
 | 
					        self.assertEqual(default.key, "default")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # same profile is returned
 | 
					        # same profile is returned
 | 
				
			||||||
        profile = self.handler.get_profile(default)
 | 
					        profile = self.handler.get_profile(default)
 | 
				
			||||||
        self.assertIs(profile, default)
 | 
					        self.assertIs(profile, default)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_collect_data_os(self):
 | 
					    def test_collect_data_os(self):
 | 
				
			||||||
        profile = self.handler.get_profile('default')
 | 
					        profile = self.handler.get_profile("default")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # typical / working scenario
 | 
					        # typical / working scenario
 | 
				
			||||||
        data = self.handler.collect_data_os(profile)
 | 
					        data = self.handler.collect_data_os(profile)
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertIn('release_id', data)
 | 
					        self.assertIn("release_id", data)
 | 
				
			||||||
        self.assertIn('release_version', data)
 | 
					        self.assertIn("release_version", data)
 | 
				
			||||||
        self.assertIn('release_full', data)
 | 
					        self.assertIn("release_full", data)
 | 
				
			||||||
        self.assertIn('timezone', data)
 | 
					        self.assertIn("timezone", data)
 | 
				
			||||||
        self.assertNotIn('errors', data)
 | 
					        self.assertNotIn("errors", data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # unreadable release path
 | 
					        # unreadable release path
 | 
				
			||||||
        data = self.handler.collect_data_os(profile, release_path='/a/path/which/does/not/exist')
 | 
					        data = self.handler.collect_data_os(
 | 
				
			||||||
 | 
					            profile, release_path="/a/path/which/does/not/exist"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertNotIn('release_id', data)
 | 
					        self.assertNotIn("release_id", data)
 | 
				
			||||||
        self.assertNotIn('release_version', data)
 | 
					        self.assertNotIn("release_version", data)
 | 
				
			||||||
        self.assertNotIn('release_full', data)
 | 
					        self.assertNotIn("release_full", data)
 | 
				
			||||||
        self.assertIn('timezone', data)
 | 
					        self.assertIn("timezone", data)
 | 
				
			||||||
        self.assertIn('errors', data)
 | 
					        self.assertIn("errors", data)
 | 
				
			||||||
        self.assertEqual(data['errors'], [
 | 
					        self.assertEqual(
 | 
				
			||||||
            "Failed to read /a/path/which/does/not/exist"
 | 
					            data["errors"], ["Failed to read /a/path/which/does/not/exist"]
 | 
				
			||||||
        ])
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # unparsable release path
 | 
					        # unparsable release path
 | 
				
			||||||
        path = self.write_file('release', "bad-content")
 | 
					        path = self.write_file("release", "bad-content")
 | 
				
			||||||
        data = self.handler.collect_data_os(profile, release_path=path)
 | 
					        data = self.handler.collect_data_os(profile, release_path=path)
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertNotIn('release_id', data)
 | 
					        self.assertNotIn("release_id", data)
 | 
				
			||||||
        self.assertNotIn('release_version', data)
 | 
					        self.assertNotIn("release_version", data)
 | 
				
			||||||
        self.assertNotIn('release_full', data)
 | 
					        self.assertNotIn("release_full", data)
 | 
				
			||||||
        self.assertIn('timezone', data)
 | 
					        self.assertIn("timezone", data)
 | 
				
			||||||
        self.assertIn('errors', data)
 | 
					        self.assertIn("errors", data)
 | 
				
			||||||
        self.assertEqual(data['errors'], [
 | 
					        self.assertEqual(data["errors"], [f"Failed to parse {path}"])
 | 
				
			||||||
            f"Failed to parse {path}"
 | 
					 | 
				
			||||||
        ])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # unreadable timezone path
 | 
					        # unreadable timezone path
 | 
				
			||||||
        data = self.handler.collect_data_os(profile, timezone_path='/a/path/which/does/not/exist')
 | 
					        data = self.handler.collect_data_os(
 | 
				
			||||||
 | 
					            profile, timezone_path="/a/path/which/does/not/exist"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertIn('release_id', data)
 | 
					        self.assertIn("release_id", data)
 | 
				
			||||||
        self.assertIn('release_version', data)
 | 
					        self.assertIn("release_version", data)
 | 
				
			||||||
        self.assertIn('release_full', data)
 | 
					        self.assertIn("release_full", data)
 | 
				
			||||||
        self.assertNotIn('timezone', data)
 | 
					        self.assertNotIn("timezone", data)
 | 
				
			||||||
        self.assertIn('errors', data)
 | 
					        self.assertIn("errors", data)
 | 
				
			||||||
        self.assertEqual(data['errors'], [
 | 
					        self.assertEqual(
 | 
				
			||||||
            "Failed to read /a/path/which/does/not/exist"
 | 
					            data["errors"], ["Failed to read /a/path/which/does/not/exist"]
 | 
				
			||||||
        ])
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_collect_data_python(self):
 | 
					    def test_collect_data_python(self):
 | 
				
			||||||
        profile = self.handler.get_profile('default')
 | 
					        profile = self.handler.get_profile("default")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # typical / working (system-wide) scenario
 | 
					        # typical / working (system-wide) scenario
 | 
				
			||||||
        data = self.handler.collect_data_python(profile)
 | 
					        data = self.handler.collect_data_python(profile)
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertNotIn('envroot', data)
 | 
					        self.assertNotIn("envroot", data)
 | 
				
			||||||
        self.assertIn('executable', data)
 | 
					        self.assertIn("executable", data)
 | 
				
			||||||
        self.assertIn('release_full', data)
 | 
					        self.assertIn("release_full", data)
 | 
				
			||||||
        self.assertIn('release_version', data)
 | 
					        self.assertIn("release_version", data)
 | 
				
			||||||
        self.assertNotIn('errors', data)
 | 
					        self.assertNotIn("errors", data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # missing executable
 | 
					        # missing executable
 | 
				
			||||||
        with patch.dict(self.config.defaults, {'wutta.telemetry.default.collect.python.executable': '/bad/path'}):
 | 
					        with patch.dict(
 | 
				
			||||||
 | 
					            self.config.defaults,
 | 
				
			||||||
 | 
					            {"wutta.telemetry.default.collect.python.executable": "/bad/path"},
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            data = self.handler.collect_data_python(profile)
 | 
					            data = self.handler.collect_data_python(profile)
 | 
				
			||||||
            self.assertIsInstance(data, dict)
 | 
					            self.assertIsInstance(data, dict)
 | 
				
			||||||
            self.assertNotIn('envroot', data)
 | 
					            self.assertNotIn("envroot", data)
 | 
				
			||||||
            self.assertIn('executable', data)
 | 
					            self.assertIn("executable", data)
 | 
				
			||||||
            self.assertNotIn('release_full', data)
 | 
					            self.assertNotIn("release_full", data)
 | 
				
			||||||
            self.assertNotIn('release_version', data)
 | 
					            self.assertNotIn("release_version", data)
 | 
				
			||||||
            self.assertIn('errors', data)
 | 
					            self.assertIn("errors", data)
 | 
				
			||||||
            self.assertEqual(data['errors'][0], "Failed to execute `python --version`")
 | 
					            self.assertEqual(data["errors"][0], "Failed to execute `python --version`")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # unparsable executable output
 | 
					        # unparsable executable output
 | 
				
			||||||
        with patch.object(mod, 'subprocess') as subprocess:
 | 
					        with patch.object(mod, "subprocess") as subprocess:
 | 
				
			||||||
            subprocess.check_output.return_value = 'bad output'.encode('utf_8')
 | 
					            subprocess.check_output.return_value = "bad output".encode("utf_8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            data = self.handler.collect_data_python(profile)
 | 
					            data = self.handler.collect_data_python(profile)
 | 
				
			||||||
            self.assertIsInstance(data, dict)
 | 
					            self.assertIsInstance(data, dict)
 | 
				
			||||||
            self.assertNotIn('envroot', data)
 | 
					            self.assertNotIn("envroot", data)
 | 
				
			||||||
            self.assertIn('executable', data)
 | 
					            self.assertIn("executable", data)
 | 
				
			||||||
            self.assertIn('release_full', data)
 | 
					            self.assertIn("release_full", data)
 | 
				
			||||||
            self.assertNotIn('release_version', data)
 | 
					            self.assertNotIn("release_version", data)
 | 
				
			||||||
            self.assertIn('errors', data)
 | 
					            self.assertIn("errors", data)
 | 
				
			||||||
            self.assertEqual(data['errors'], [
 | 
					            self.assertEqual(
 | 
				
			||||||
                "Failed to parse Python version",
 | 
					                data["errors"],
 | 
				
			||||||
            ])
 | 
					                [
 | 
				
			||||||
 | 
					                    "Failed to parse Python version",
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # typical / working (virtual environment) scenario
 | 
					        # typical / working (virtual environment) scenario
 | 
				
			||||||
        self.config.setdefault('wutta.telemetry.default.collect.python.envroot', '/srv/envs/poser')
 | 
					        self.config.setdefault(
 | 
				
			||||||
 | 
					            "wutta.telemetry.default.collect.python.envroot", "/srv/envs/poser"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        data = self.handler.collect_data_python(profile)
 | 
					        data = self.handler.collect_data_python(profile)
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertIn('executable', data)
 | 
					        self.assertIn("executable", data)
 | 
				
			||||||
        self.assertEqual(data['executable'], '/srv/envs/poser/bin/python')
 | 
					        self.assertEqual(data["executable"], "/srv/envs/poser/bin/python")
 | 
				
			||||||
        self.assertNotIn('release_full', data)
 | 
					        self.assertNotIn("release_full", data)
 | 
				
			||||||
        self.assertNotIn('release_version', data)
 | 
					        self.assertNotIn("release_version", data)
 | 
				
			||||||
        self.assertIn('errors', data)
 | 
					        self.assertIn("errors", data)
 | 
				
			||||||
        self.assertEqual(data['errors'][0], "Failed to execute `python --version`")
 | 
					        self.assertEqual(data["errors"][0], "Failed to execute `python --version`")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_normalize_errors(self):
 | 
					    def test_normalize_errors(self):
 | 
				
			||||||
        data = {
 | 
					        data = {
 | 
				
			||||||
            'os': {
 | 
					            "os": {
 | 
				
			||||||
                'timezone': 'America/Chicago',
 | 
					                "timezone": "America/Chicago",
 | 
				
			||||||
                'errors': [
 | 
					                "errors": [
 | 
				
			||||||
                    "Failed to read /etc/os-release",
 | 
					                    "Failed to read /etc/os-release",
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            'python': {
 | 
					            "python": {
 | 
				
			||||||
                'executable': '/usr/bin/python3',
 | 
					                "executable": "/usr/bin/python3",
 | 
				
			||||||
                'errors': [
 | 
					                "errors": [
 | 
				
			||||||
                    "Failed to run `python --version`",
 | 
					                    "Failed to run `python --version`",
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.handler.normalize_errors(data)
 | 
					        self.handler.normalize_errors(data)
 | 
				
			||||||
        self.assertIn('os', data)
 | 
					        self.assertIn("os", data)
 | 
				
			||||||
        self.assertIn('python', data)
 | 
					        self.assertIn("python", data)
 | 
				
			||||||
        self.assertIn('errors', data)
 | 
					        self.assertIn("errors", data)
 | 
				
			||||||
        self.assertEqual(data['errors'], [
 | 
					        self.assertEqual(
 | 
				
			||||||
            "Failed to read /etc/os-release",
 | 
					            data["errors"],
 | 
				
			||||||
            "Failed to run `python --version`",
 | 
					            [
 | 
				
			||||||
        ])
 | 
					                "Failed to read /etc/os-release",
 | 
				
			||||||
 | 
					                "Failed to run `python --version`",
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_collect_all_data(self):
 | 
					    def test_collect_all_data(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # typical / working scenario
 | 
					        # typical / working scenario
 | 
				
			||||||
        data = self.handler.collect_all_data()
 | 
					        data = self.handler.collect_all_data()
 | 
				
			||||||
        self.assertIsInstance(data, dict)
 | 
					        self.assertIsInstance(data, dict)
 | 
				
			||||||
        self.assertIn('os', data)
 | 
					        self.assertIn("os", data)
 | 
				
			||||||
        self.assertIn('python', data)
 | 
					        self.assertIn("python", data)
 | 
				
			||||||
        self.assertNotIn('errors', data)
 | 
					        self.assertNotIn("errors", data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_submit_all_data(self):
 | 
					    def test_submit_all_data(self):
 | 
				
			||||||
        profile = self.handler.get_profile('default')
 | 
					        profile = self.handler.get_profile("default")
 | 
				
			||||||
        profile.submit_url = '/testing'
 | 
					        profile.submit_url = "/testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with patch.object(mod, 'SimpleAPIClient') as SimpleAPIClient:
 | 
					        with patch.object(mod, "SimpleAPIClient") as SimpleAPIClient:
 | 
				
			||||||
            client = MagicMock()
 | 
					            client = MagicMock()
 | 
				
			||||||
            SimpleAPIClient.return_value = client
 | 
					            SimpleAPIClient.return_value = client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # collecting all data
 | 
					            # collecting all data
 | 
				
			||||||
            with patch.object(self.handler, 'collect_all_data') as collect_all_data:
 | 
					            with patch.object(self.handler, "collect_all_data") as collect_all_data:
 | 
				
			||||||
                collect_all_data.return_value = []
 | 
					                collect_all_data.return_value = []
 | 
				
			||||||
                self.handler.submit_all_data(profile)
 | 
					                self.handler.submit_all_data(profile)
 | 
				
			||||||
                collect_all_data.assert_called_once_with(profile)
 | 
					                collect_all_data.assert_called_once_with(profile)
 | 
				
			||||||
                client.post.assert_called_once_with('/testing', data=[])
 | 
					                client.post.assert_called_once_with("/testing", data=[])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # use data from caller
 | 
					            # use data from caller
 | 
				
			||||||
            client.post.reset_mock()
 | 
					            client.post.reset_mock()
 | 
				
			||||||
            self.handler.submit_all_data(profile, data=['foo'])
 | 
					            self.handler.submit_all_data(profile, data=["foo"])
 | 
				
			||||||
            client.post.assert_called_once_with('/testing', data=['foo'])
 | 
					            client.post.assert_called_once_with("/testing", data=["foo"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestTelemetryProfile(ConfigTestCase):
 | 
					class TestTelemetryProfile(ConfigTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_profile(self, key='default'):
 | 
					    def make_profile(self, key="default"):
 | 
				
			||||||
        return mod.TelemetryProfile(self.config, key)
 | 
					        return mod.TelemetryProfile(self.config, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_section(self):
 | 
					    def test_section(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # default
 | 
					        # default
 | 
				
			||||||
        profile = self.make_profile()
 | 
					        profile = self.make_profile()
 | 
				
			||||||
        self.assertEqual(profile.section, 'wutta.telemetry')
 | 
					        self.assertEqual(profile.section, "wutta.telemetry")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # custom appname
 | 
					        # custom appname
 | 
				
			||||||
        with patch.object(self.config, 'appname', new='wuttatest'):
 | 
					        with patch.object(self.config, "appname", new="wuttatest"):
 | 
				
			||||||
            profile = self.make_profile()
 | 
					            profile = self.make_profile()
 | 
				
			||||||
            self.assertEqual(profile.section, 'wuttatest.telemetry')
 | 
					            self.assertEqual(profile.section, "wuttatest.telemetry")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_load(self):
 | 
					    def test_load(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # defaults
 | 
					        # defaults
 | 
				
			||||||
        profile = self.make_profile()
 | 
					        profile = self.make_profile()
 | 
				
			||||||
        self.assertEqual(profile.collect_keys, ['os', 'python'])
 | 
					        self.assertEqual(profile.collect_keys, ["os", "python"])
 | 
				
			||||||
        self.assertIsNone(profile.submit_url)
 | 
					        self.assertIsNone(profile.submit_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # configured
 | 
					        # configured
 | 
				
			||||||
        self.config.setdefault('wutta.telemetry.default.collect.keys', 'os,network,python')
 | 
					        self.config.setdefault(
 | 
				
			||||||
        self.config.setdefault('wutta.telemetry.default.submit.url', '/nodes/telemetry')
 | 
					            "wutta.telemetry.default.collect.keys", "os,network,python"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.config.setdefault("wutta.telemetry.default.submit.url", "/nodes/telemetry")
 | 
				
			||||||
        profile = self.make_profile()
 | 
					        profile = self.make_profile()
 | 
				
			||||||
        self.assertEqual(profile.collect_keys, ['os', 'network', 'python'])
 | 
					        self.assertEqual(profile.collect_keys, ["os", "network", "python"])
 | 
				
			||||||
        self.assertEqual(profile.submit_url, '/nodes/telemetry')
 | 
					        self.assertEqual(profile.submit_url, "/nodes/telemetry")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue