WuttaTell 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wuttatell/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # WuttaTell -- Telemetry submission for Wutta Framework
5
+ # Copyright © 2025 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ WuttaTell -- Telemetry submission for Wutta Framework
25
+ """
26
+
27
+ from ._version import __version__
wuttatell/_version.py ADDED
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8; -*-
2
+ """
3
+ Package Version
4
+ """
5
+
6
+ from importlib.metadata import version
7
+
8
+
9
+ __version__ = version("WuttaTell")
wuttatell/app.py ADDED
@@ -0,0 +1,57 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # WuttaTell -- Telemetry submission for Wutta Framework
5
+ # Copyright © 2025-2026 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ App Provider
25
+ """
26
+
27
+ import logging
28
+
29
+ from wuttjamaican.app import AppProvider
30
+
31
+
32
+ log = logging.getLogger(__name__)
33
+
34
+
35
+ class WuttaTellAppProvider(AppProvider):
36
+ """
37
+ The :term:`app provider` for WuttaTell.
38
+
39
+ This adds the :meth:`get_telemetry_handler()` method for the
40
+ :term:`app handler`.
41
+ """
42
+
43
+ def get_telemetry_handler(self, **kwargs):
44
+ """
45
+ Get the configured telemetry handler.
46
+
47
+ :rtype: :class:`~wuttatell.telemetry.TelemetryHandler`
48
+ """
49
+ if "telemetry" not in self.app.handlers:
50
+ spec = self.config.get(
51
+ f"{self.appname}.telemetry.handler",
52
+ default="wuttatell.telemetry:TelemetryHandler",
53
+ )
54
+ log.debug("telemetry handler spec is %s", spec)
55
+ factory = self.app.load_object(spec)
56
+ self.app.handlers["telemetry"] = factory(self.config, **kwargs)
57
+ return self.app.handlers["telemetry"]
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # WuttaTell -- Telemetry submission for Wutta Framework
5
+ # Copyright © 2025 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ WuttaTell - ``wutta`` subcommands
25
+ """
26
+
27
+ # nb. must bring in all modules for discovery to work
28
+ from . import tell
wuttatell/cli/tell.py ADDED
@@ -0,0 +1,91 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # WuttaTell -- Telemetry submission for Wutta Framework
5
+ # Copyright © 2025-2026 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ See also: :ref:`wutta-tell`
25
+ """
26
+
27
+ import logging
28
+
29
+ try:
30
+ from typing import Annotated
31
+ except ImportError: # pragma: no cover
32
+ from typing_extensions import Annotated
33
+
34
+ import typer
35
+
36
+ from wuttjamaican.cli import wutta_typer
37
+
38
+
39
+ log = logging.getLogger(__name__)
40
+
41
+
42
+ @wutta_typer.command()
43
+ def tell(
44
+ ctx: typer.Context,
45
+ profile: Annotated[
46
+ str,
47
+ typer.Option(
48
+ "--profile",
49
+ "-p",
50
+ help="Profile indicating which data to collect and where to submit it. "
51
+ "If not specified, default profile is assumed.",
52
+ ),
53
+ ] = None,
54
+ list_profiles: Annotated[
55
+ bool,
56
+ typer.Option(
57
+ "--list",
58
+ "-l",
59
+ help="List defined telemetry profiles.",
60
+ ),
61
+ ] = False,
62
+ dry_run: Annotated[
63
+ bool,
64
+ typer.Option(
65
+ "--dry-run",
66
+ help="Go through motions and log actions, but do not submit data.",
67
+ ),
68
+ ] = False,
69
+ ):
70
+ """
71
+ Collect and submit telemetry data
72
+ """
73
+ config = ctx.parent.wutta_config
74
+ app = config.get_app()
75
+ telemetry = app.get_telemetry_handler()
76
+
77
+ if list_profiles:
78
+ print("telemetry profiles:")
79
+ print("-------------------------")
80
+ for key in telemetry.get_all_profile_keys():
81
+ profile = telemetry.get_profile(key)
82
+ print(key, profile.collect_keys)
83
+ return
84
+
85
+ data = telemetry.collect_all_data(profile=profile)
86
+
87
+ if dry_run:
88
+ log.info("dry run, so will not submit data to server")
89
+ else:
90
+ telemetry.submit_all_data(profile=profile, data=data)
91
+ log.info("data submitted okay")
wuttatell/client.py ADDED
@@ -0,0 +1,207 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # WuttaTell -- Telemetry submission for Wutta Framework
5
+ # Copyright © 2025 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ Simple API Client
25
+ """
26
+
27
+ import json
28
+ from urllib.parse import urlparse
29
+
30
+ import requests
31
+
32
+
33
+ class SimpleAPIClient:
34
+ """
35
+ Simple client for "typical" API service.
36
+
37
+ This basically assumes telemetry can be submitted to a single API
38
+ endpoint, and the request should contain an auth token.
39
+
40
+ :param config: App :term:`config object`.
41
+
42
+ :param base_url: Base URL of the API.
43
+
44
+ :param token: Auth token for the API.
45
+
46
+ :param ssl_verify: Whether the SSL cert presented by the server
47
+ should be verified. This is effectively true by default, but
48
+ may be disabled for testing with self-signed certs etc.
49
+
50
+ :param max_retries: Maximum number of retries each connection
51
+ should attempt. This value is ultimately given to the
52
+ :class:`~requests:requests.adapters.HTTPAdapter` instance.
53
+
54
+ Most params may be omitted, if config specifies instead:
55
+
56
+ .. code-block:: ini
57
+
58
+ [wutta.api]
59
+ base_url = https://my.example.com/api
60
+ token = XYZPDQ12345
61
+ ssl_verify = false
62
+ max_retries = 5
63
+
64
+ Upon instantiation, :attr:`session` will be ``None`` until the
65
+ first request is made. (Technically when :meth:`init_session()`
66
+ first happens.)
67
+
68
+ .. attribute:: session
69
+
70
+ :class:`requests:requests.Session` instance being used to make
71
+ API requests.
72
+ """
73
+
74
+ def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
75
+ self, config, base_url=None, token=None, ssl_verify=None, max_retries=None
76
+ ):
77
+ self.config = config
78
+
79
+ self.base_url = base_url or self.config.require(
80
+ f"{self.config.appname}.api.base_url"
81
+ )
82
+ self.base_url = self.base_url.rstrip("/")
83
+ self.token = token or self.config.require(f"{self.config.appname}.api.token")
84
+
85
+ if max_retries is not None:
86
+ self.max_retries = max_retries
87
+ else:
88
+ self.max_retries = self.config.get_int(
89
+ f"{self.config.appname}.api.max_retries"
90
+ )
91
+
92
+ if ssl_verify is not None:
93
+ self.ssl_verify = ssl_verify
94
+ else:
95
+ self.ssl_verify = self.config.get_bool(
96
+ f"{self.config.appname}.api.ssl_verify", default=True
97
+ )
98
+
99
+ self.session = None
100
+
101
+ def init_session(self):
102
+ """
103
+ Initialize the HTTP session with the API.
104
+
105
+ This method is invoked as part of :meth:`make_request()`.
106
+
107
+ It first checks :attr:`session` and will skip if already initialized.
108
+
109
+ For initialization, it establishes a new
110
+ :class:`requests:requests.Session` instance, and modifies it
111
+ as needed per config.
112
+ """
113
+ if self.session:
114
+ return
115
+
116
+ self.session = requests.Session()
117
+
118
+ # maybe *disable* SSL cert verification
119
+ # (should only be used for testing e.g. w/ self-signed certs)
120
+ if not self.ssl_verify:
121
+ self.session.verify = False
122
+
123
+ # maybe set max retries, e.g. for flaky connections
124
+ if self.max_retries is not None:
125
+ adapter = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
126
+ self.session.mount(self.base_url, adapter)
127
+
128
+ # TODO: is this a good idea, or hacky security risk..?
129
+ # without it, can get error response:
130
+ # 400 Client Error: Bad CSRF Origin for url
131
+ parts = urlparse(self.base_url)
132
+ self.session.headers.update(
133
+ {
134
+ "Origin": f"{parts.scheme}://{parts.netloc}",
135
+ }
136
+ )
137
+
138
+ # authenticate via token only (for now?)
139
+ self.session.headers.update(
140
+ {
141
+ "Authorization": f"Bearer {self.token}",
142
+ }
143
+ )
144
+
145
+ def make_request(self, request_method, api_method, params=None, data=None):
146
+ """
147
+ Make a request to the API, and return the response.
148
+
149
+ This first calls :meth:`init_session()` to establish the
150
+ session if needed.
151
+
152
+ :param request_method: HTTP request method; for now only
153
+ ``'GET'`` and ``'POST'`` are supported.
154
+
155
+ :param api_method: API method endpoint to use,
156
+ e.g. ``'/my/telemetry'``
157
+
158
+ :param params: Dict of query string params for the request, if
159
+ applicable.
160
+
161
+ :param data: Payload data for the request, if applicable.
162
+ Should be JSON-serializable, e.g. a list or dict.
163
+
164
+ :rtype: :class:`requests:requests.Response` instance.
165
+ """
166
+ self.init_session()
167
+ api_method = api_method.lstrip("/")
168
+ url = f"{self.base_url}/{api_method}"
169
+ if request_method == "GET":
170
+ response = self.session.get(url, params=params)
171
+ elif request_method == "POST":
172
+ response = self.session.post(url, params=params, data=json.dumps(data))
173
+ else:
174
+ raise NotImplementedError(f"unsupported request method: {request_method}")
175
+ response.raise_for_status()
176
+ return response
177
+
178
+ def get(self, api_method, params=None):
179
+ """
180
+ Perform a GET request for the given API method, and return the
181
+ response.
182
+
183
+ This calls :meth:`make_request()` for the heavy lifting.
184
+
185
+ :param api_method: API method endpoint to use,
186
+ e.g. ``'/my/telemetry'``
187
+
188
+ :param params: Dict of query string params for the request, if
189
+ applicable.
190
+
191
+ :rtype: :class:`requests:requests.Response` instance.
192
+ """
193
+ return self.make_request("GET", api_method, params=params)
194
+
195
+ def post(self, api_method, **kwargs):
196
+ """
197
+ Perform a POST request for the given API method, and return
198
+ the response.
199
+
200
+ This calls :meth:`make_request()` for the heavy lifting.
201
+
202
+ :param api_method: API method endpoint to use,
203
+ e.g. ``'/my/telemetry'``
204
+
205
+ :rtype: :class:`requests:requests.Response` instance.
206
+ """
207
+ return self.make_request("POST", api_method, **kwargs)