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 +27 -0
- wuttatell/_version.py +9 -0
- wuttatell/app.py +57 -0
- wuttatell/cli/__init__.py +28 -0
- wuttatell/cli/tell.py +91 -0
- wuttatell/client.py +207 -0
- wuttatell/telemetry.py +357 -0
- wuttatell-0.3.1.dist-info/METADATA +43 -0
- wuttatell-0.3.1.dist-info/RECORD +12 -0
- wuttatell-0.3.1.dist-info/WHEEL +4 -0
- wuttatell-0.3.1.dist-info/entry_points.txt +5 -0
- wuttatell-0.3.1.dist-info/licenses/COPYING.txt +674 -0
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
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)
|