pamoja-telemetry 0.1.18__tar.gz
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.
- pamoja_telemetry-0.1.18/.gitignore +13 -0
- pamoja_telemetry-0.1.18/LICENSE-MIT +21 -0
- pamoja_telemetry-0.1.18/PKG-INFO +94 -0
- pamoja_telemetry-0.1.18/README.md +76 -0
- pamoja_telemetry-0.1.18/pamoja/telemetry/__init__.py +149 -0
- pamoja_telemetry-0.1.18/pamoja/telemetry/py.typed +0 -0
- pamoja_telemetry-0.1.18/pyproject.toml +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthony Wiedman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pamoja-telemetry
|
|
3
|
+
Version: 0.1.18
|
|
4
|
+
Summary: Observability that ships only what is worth the bytes as link cost rises, while counting everything.
|
|
5
|
+
Project-URL: Repository, https://github.com/molexxxx/pamoja
|
|
6
|
+
Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/telemetry.html
|
|
7
|
+
Author: molexxxx
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE-MIT
|
|
10
|
+
Keywords: iot,pamoja,robotics,telemetry
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: pamoja-native==0.1.18
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# pamoja-telemetry
|
|
20
|
+
|
|
21
|
+
Observability that ships only what is worth the bytes as link cost rises, while counting everything. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
|
|
22
|
+
|
|
23
|
+
[](https://pamoja.molex.cloud/docs/guides/telemetry.html)
|
|
24
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
25
|
+
[](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install pamoja-telemetry
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from pamoja import telemetry
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
|
|
38
|
+
|
|
39
|
+
## Example
|
|
40
|
+
|
|
41
|
+
The script the test suite runs, spliced here as it ran.
|
|
42
|
+
|
|
43
|
+
From [`bindings/python/guides/telemetry.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/telemetry.py):
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pamoja.telemetry import Event, Level, LinkCost, Reporter, link_cost_threshold
|
|
47
|
+
|
|
48
|
+
# The node is willing to record everything, then finds out it is reporting over a metered
|
|
49
|
+
# link, which puts the bar at INFO.
|
|
50
|
+
reporter = Reporter(Level.TRACE)
|
|
51
|
+
reporter.adapt_to(LinkCost.METERED)
|
|
52
|
+
print(f"on a metered link, nothing below {reporter.threshold.value} is sent")
|
|
53
|
+
|
|
54
|
+
# Routine detail stops going out. A reading and the warning that follows it still do, and
|
|
55
|
+
# a shipped event comes back with the measurement that triggered it.
|
|
56
|
+
tick = reporter.record(Event(Level.DEBUG, "loop.tick"))
|
|
57
|
+
reading = reporter.record(Event(Level.INFO, "reading.ok", 4.8))
|
|
58
|
+
print(f"loop.tick sent: {tick is not None}")
|
|
59
|
+
print(f"reading.ok sent: {reading is not None}")
|
|
60
|
+
warned = reporter.record(Event(Level.WARN, "battery.low", 0.18))
|
|
61
|
+
print(f"sent {warned.code} carrying {warned.value}")
|
|
62
|
+
|
|
63
|
+
# The node falls back to satellite, which raises the bar to WARN. The same reading is no
|
|
64
|
+
# longer worth its bytes; a failure still is.
|
|
65
|
+
reporter.adapt_to(LinkCost.EXPENSIVE)
|
|
66
|
+
dearer = reporter.record(Event(Level.INFO, "reading.ok", 4.9))
|
|
67
|
+
lost = reporter.record(Event(Level.ERROR, "link.lost"))
|
|
68
|
+
print(f"on satellite, reading.ok sent: {dearer is not None}")
|
|
69
|
+
print(f"on satellite, link.lost sent: {lost is not None}")
|
|
70
|
+
|
|
71
|
+
# Only the stream was thinned, not the counts, so every event is still accounted for and
|
|
72
|
+
# the snapshot is what the node ships in place of them.
|
|
73
|
+
counts = reporter.snapshot()
|
|
74
|
+
print(f"of {reporter.total} events, {counts.emitted} went out and {counts.dropped} were counted only")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## The same capability in every language
|
|
78
|
+
|
|
79
|
+
| Language | Package | Reference |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| Rust | [`pamoja-telemetry`](https://crates.io/crates/pamoja-telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_telemetry/index.html), [docs.rs](https://docs.rs/pamoja-telemetry), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-telemetry) |
|
|
82
|
+
| TypeScript | [`@pamoja/telemetry`](https://www.npmjs.com/package/@pamoja/telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-telemetry) |
|
|
83
|
+
| Python | [`pamoja-telemetry`](https://pypi.org/project/pamoja-telemetry/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-telemetry) |
|
|
84
|
+
| C# | [`Pamoja.Telemetry`](https://www.nuget.org/packages/Pamoja.Telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-telemetry) |
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
- [`pamoja.telemetry` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html), every class and function in this module.
|
|
89
|
+
- [The Telemetry guide](https://pamoja.molex.cloud/docs/guides/telemetry.html), with the same example in Rust, TypeScript, and C#.
|
|
90
|
+
- [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# pamoja-telemetry
|
|
2
|
+
|
|
3
|
+
Observability that ships only what is worth the bytes as link cost rises, while counting everything. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
|
|
4
|
+
|
|
5
|
+
[](https://pamoja.molex.cloud/docs/guides/telemetry.html)
|
|
6
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
7
|
+
[](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install pamoja-telemetry
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pamoja import telemetry
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
The script the test suite runs, spliced here as it ran.
|
|
24
|
+
|
|
25
|
+
From [`bindings/python/guides/telemetry.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/telemetry.py):
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pamoja.telemetry import Event, Level, LinkCost, Reporter, link_cost_threshold
|
|
29
|
+
|
|
30
|
+
# The node is willing to record everything, then finds out it is reporting over a metered
|
|
31
|
+
# link, which puts the bar at INFO.
|
|
32
|
+
reporter = Reporter(Level.TRACE)
|
|
33
|
+
reporter.adapt_to(LinkCost.METERED)
|
|
34
|
+
print(f"on a metered link, nothing below {reporter.threshold.value} is sent")
|
|
35
|
+
|
|
36
|
+
# Routine detail stops going out. A reading and the warning that follows it still do, and
|
|
37
|
+
# a shipped event comes back with the measurement that triggered it.
|
|
38
|
+
tick = reporter.record(Event(Level.DEBUG, "loop.tick"))
|
|
39
|
+
reading = reporter.record(Event(Level.INFO, "reading.ok", 4.8))
|
|
40
|
+
print(f"loop.tick sent: {tick is not None}")
|
|
41
|
+
print(f"reading.ok sent: {reading is not None}")
|
|
42
|
+
warned = reporter.record(Event(Level.WARN, "battery.low", 0.18))
|
|
43
|
+
print(f"sent {warned.code} carrying {warned.value}")
|
|
44
|
+
|
|
45
|
+
# The node falls back to satellite, which raises the bar to WARN. The same reading is no
|
|
46
|
+
# longer worth its bytes; a failure still is.
|
|
47
|
+
reporter.adapt_to(LinkCost.EXPENSIVE)
|
|
48
|
+
dearer = reporter.record(Event(Level.INFO, "reading.ok", 4.9))
|
|
49
|
+
lost = reporter.record(Event(Level.ERROR, "link.lost"))
|
|
50
|
+
print(f"on satellite, reading.ok sent: {dearer is not None}")
|
|
51
|
+
print(f"on satellite, link.lost sent: {lost is not None}")
|
|
52
|
+
|
|
53
|
+
# Only the stream was thinned, not the counts, so every event is still accounted for and
|
|
54
|
+
# the snapshot is what the node ships in place of them.
|
|
55
|
+
counts = reporter.snapshot()
|
|
56
|
+
print(f"of {reporter.total} events, {counts.emitted} went out and {counts.dropped} were counted only")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The same capability in every language
|
|
60
|
+
|
|
61
|
+
| Language | Package | Reference |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| Rust | [`pamoja-telemetry`](https://crates.io/crates/pamoja-telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_telemetry/index.html), [docs.rs](https://docs.rs/pamoja-telemetry), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-telemetry) |
|
|
64
|
+
| TypeScript | [`@pamoja/telemetry`](https://www.npmjs.com/package/@pamoja/telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-telemetry) |
|
|
65
|
+
| Python | [`pamoja-telemetry`](https://pypi.org/project/pamoja-telemetry/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-telemetry) |
|
|
66
|
+
| C# | [`Pamoja.Telemetry`](https://www.nuget.org/packages/Pamoja.Telemetry) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Telemetry.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-telemetry) |
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
- [`pamoja.telemetry` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/telemetry.html), every class and function in this module.
|
|
71
|
+
- [The Telemetry guide](https://pamoja.molex.cloud/docs/guides/telemetry.html), with the same example in Rust, TypeScript, and C#.
|
|
72
|
+
- [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Idiomatic telemetry facade.
|
|
2
|
+
|
|
3
|
+
A node that ships every event it produces will spend more on reporting than on
|
|
4
|
+
the job it was installed to do, and on a satellite link that is money. A reporter
|
|
5
|
+
ships what is worth its bytes, counts everything either way, and moves its own
|
|
6
|
+
bar as the link gets more expensive, so the aggregate picture survives even when
|
|
7
|
+
the detail cannot be sent.
|
|
8
|
+
|
|
9
|
+
The generated binding decides on the level alone, since that is all the core
|
|
10
|
+
reporter reads. The event a caller writes travels no further than this layer,
|
|
11
|
+
which hands it straight back when it should be shipped.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import enum
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from pamoja._native import Reporter as _Reporter
|
|
20
|
+
from pamoja._native import Snapshot
|
|
21
|
+
from pamoja._native import link_cost_threshold as _link_cost_threshold
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Event",
|
|
25
|
+
"Level",
|
|
26
|
+
"LinkCost",
|
|
27
|
+
"Reporter",
|
|
28
|
+
"Snapshot",
|
|
29
|
+
"link_cost_threshold",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Level(str, enum.Enum):
|
|
34
|
+
"""How urgent an event is."""
|
|
35
|
+
|
|
36
|
+
#: Fine-grained detail, useful only when chasing a specific problem.
|
|
37
|
+
TRACE = "Trace"
|
|
38
|
+
#: Diagnostic detail for development.
|
|
39
|
+
DEBUG = "Debug"
|
|
40
|
+
#: A normal, noteworthy event.
|
|
41
|
+
INFO = "Info"
|
|
42
|
+
#: Something unexpected that the node recovered from.
|
|
43
|
+
WARN = "Warn"
|
|
44
|
+
#: A failure that needs attention.
|
|
45
|
+
ERROR = "Error"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LinkCost(str, enum.Enum):
|
|
49
|
+
"""What the link back to the network currently costs."""
|
|
50
|
+
|
|
51
|
+
#: Bytes are effectively free, such as on wired power and ethernet.
|
|
52
|
+
FREE = "Free"
|
|
53
|
+
#: Bytes are paid for, such as on a cellular plan.
|
|
54
|
+
METERED = "Metered"
|
|
55
|
+
#: Bytes are scarce, such as on a satellite or long-range radio link.
|
|
56
|
+
EXPENSIVE = "Expensive"
|
|
57
|
+
#: Nothing can be shipped at all.
|
|
58
|
+
OFFLINE = "Offline"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Event:
|
|
63
|
+
"""A structured telemetry event.
|
|
64
|
+
|
|
65
|
+
The code is a stable, short label such as ``battery.low`` rather than a
|
|
66
|
+
free-form message, so events stay small and group cleanly into counts.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
#: How urgent the event is.
|
|
70
|
+
level: Level
|
|
71
|
+
#: A stable, short identifier for what happened.
|
|
72
|
+
code: str
|
|
73
|
+
#: An optional measurement, such as the charge that triggered it.
|
|
74
|
+
value: float | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def link_cost_threshold(cost: LinkCost) -> Level:
|
|
78
|
+
"""Return the level a link cost calls for.
|
|
79
|
+
|
|
80
|
+
:param cost: What the link currently costs.
|
|
81
|
+
:returns: The lowest level still worth its bytes at that cost.
|
|
82
|
+
"""
|
|
83
|
+
return Level(_link_cost_threshold(cost.value))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Reporter:
|
|
87
|
+
"""Record events, ship the ones worth their bytes, and count them all."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, threshold: Level = Level.INFO) -> None:
|
|
90
|
+
"""Create a reporter that ships events at or above ``threshold``.
|
|
91
|
+
|
|
92
|
+
:param threshold: The lowest level to ship.
|
|
93
|
+
"""
|
|
94
|
+
self._inner = _Reporter(threshold.value)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def threshold(self) -> Level:
|
|
98
|
+
"""The level this reporter is currently shipping from."""
|
|
99
|
+
return Level(self._inner.threshold)
|
|
100
|
+
|
|
101
|
+
@threshold.setter
|
|
102
|
+
def threshold(self, threshold: Level) -> None:
|
|
103
|
+
self._inner.threshold = threshold.value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def total(self) -> int:
|
|
107
|
+
"""How many events have been seen across every level."""
|
|
108
|
+
return self._inner.total
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def emitted(self) -> int:
|
|
112
|
+
"""How many events passed the threshold and were shipped."""
|
|
113
|
+
return self._inner.emitted
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def dropped(self) -> int:
|
|
117
|
+
"""How many events the threshold dropped."""
|
|
118
|
+
return self._inner.dropped
|
|
119
|
+
|
|
120
|
+
def adapt_to(self, cost: LinkCost) -> None:
|
|
121
|
+
"""Move the threshold to match what the link now costs.
|
|
122
|
+
|
|
123
|
+
:param cost: What the link currently costs.
|
|
124
|
+
"""
|
|
125
|
+
self._inner.adapt_to(cost.value)
|
|
126
|
+
|
|
127
|
+
def record(self, event: Event) -> Event | None:
|
|
128
|
+
"""Record an event, returning it when it should be shipped.
|
|
129
|
+
|
|
130
|
+
:param event: The event that occurred.
|
|
131
|
+
:returns: The same event when it passed the threshold, or ``None`` when
|
|
132
|
+
it was counted and dropped.
|
|
133
|
+
"""
|
|
134
|
+
return event if self._inner.record(event.level.value) else None
|
|
135
|
+
|
|
136
|
+
def count(self, level: Level) -> int:
|
|
137
|
+
"""Return how many events have been seen at a level, shipped or not.
|
|
138
|
+
|
|
139
|
+
:param level: The level to count.
|
|
140
|
+
:returns: The number of events recorded at that level.
|
|
141
|
+
"""
|
|
142
|
+
return self._inner.count(level.value)
|
|
143
|
+
|
|
144
|
+
def snapshot(self) -> Snapshot:
|
|
145
|
+
"""Take a snapshot of the counters to ship in place of the stream.
|
|
146
|
+
|
|
147
|
+
:returns: The per-level counts and the shipped and dropped totals.
|
|
148
|
+
"""
|
|
149
|
+
return self._inner.snapshot()
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pamoja-telemetry"
|
|
7
|
+
version = "0.1.18"
|
|
8
|
+
description = "Observability that ships only what is worth the bytes as link cost rises, while counting everything."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
license-files = ["LICENSE-MIT"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "molexxxx" }]
|
|
14
|
+
keywords = ["pamoja", "iot", "robotics", "telemetry"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pamoja-native==0.1.18",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/molexxxx/pamoja"
|
|
27
|
+
Documentation = "https://pamoja.molex.cloud/docs/guides/telemetry.html"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["pamoja"]
|