fleetmesh-node 0.2.0__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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Class-driven problem and runtime trouble definitions for compartments.
|
|
2
|
+
|
|
3
|
+
Every compartment declares what it is (SCHEMA, COMMANDS, NEEDS, STORES,
|
|
4
|
+
WORKERS, EVERY, CHANNELS, METRICS, LIFECYCLE). When a declaration is invalid,
|
|
5
|
+
`declaration_problems` collects `Problem` instances rather than loose strings,
|
|
6
|
+
carrying the surface, semantic code, target, and human-readable message.
|
|
7
|
+
|
|
8
|
+
Similarly, when a compartment is in an abnormal state, `trouble()` returns
|
|
9
|
+
`Trouble` instances carrying the fault code, severity, timestamp, and optional
|
|
10
|
+
remediation hint.
|
|
11
|
+
|
|
12
|
+
Both `Problem` and `Trouble` inherit from `str` so that:
|
|
13
|
+
- String formatting (`str(p)`, `f"{p}"`) produces the sentence directly.
|
|
14
|
+
- Substring checks (`'twice' in p`, `'relay' in t`) continue to pass.
|
|
15
|
+
- List comparisons (`[p] == ['message']`) evaluate True against raw strings.
|
|
16
|
+
- Existing callers, tests, and json encoders require zero breaking changes.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Mapping, Optional, TypeVar, TypedDict
|
|
21
|
+
|
|
22
|
+
P = TypeVar('P', bound='Problem')
|
|
23
|
+
T_tr = TypeVar('T_tr', bound='Trouble')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProblemDict(TypedDict, total=False):
|
|
27
|
+
"""Concrete dictionary structure for a declaration problem."""
|
|
28
|
+
|
|
29
|
+
message: str
|
|
30
|
+
surface: str
|
|
31
|
+
code: str
|
|
32
|
+
target: str
|
|
33
|
+
hint: Optional[str]
|
|
34
|
+
reference: str
|
|
35
|
+
expected: str
|
|
36
|
+
got: str
|
|
37
|
+
owner: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Problem(str):
|
|
41
|
+
"""A structured declaration problem.
|
|
42
|
+
|
|
43
|
+
Subclasses str for complete backwards compatibility with tests and callers
|
|
44
|
+
expecting str messages, while exposing structured metadata for linters,
|
|
45
|
+
checkers, and IDE diagnostics.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
surface: str
|
|
49
|
+
code: str
|
|
50
|
+
target: str
|
|
51
|
+
message: str
|
|
52
|
+
hint: Optional[str]
|
|
53
|
+
|
|
54
|
+
def __new__(
|
|
55
|
+
cls: type[P],
|
|
56
|
+
message: str,
|
|
57
|
+
*,
|
|
58
|
+
surface: str = '',
|
|
59
|
+
code: str = '',
|
|
60
|
+
target: str = '',
|
|
61
|
+
hint: Optional[str] = None,
|
|
62
|
+
) -> P:
|
|
63
|
+
obj = super().__new__(cls, message)
|
|
64
|
+
obj.surface = str(surface)
|
|
65
|
+
obj.code = str(code)
|
|
66
|
+
obj.target = str(target)
|
|
67
|
+
obj.message = str(message)
|
|
68
|
+
obj.hint = hint
|
|
69
|
+
return obj
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> ProblemDict:
|
|
72
|
+
"""Dictionary representation for structured reporting."""
|
|
73
|
+
d: ProblemDict = {
|
|
74
|
+
'message': self.message,
|
|
75
|
+
'surface': self.surface,
|
|
76
|
+
'code': self.code,
|
|
77
|
+
}
|
|
78
|
+
if self.target:
|
|
79
|
+
d['target'] = self.target
|
|
80
|
+
if self.hint is not None:
|
|
81
|
+
d['hint'] = self.hint
|
|
82
|
+
return d
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DuplicateProblem(Problem):
|
|
86
|
+
"""An item was declared twice on the same surface."""
|
|
87
|
+
|
|
88
|
+
def __new__(
|
|
89
|
+
cls,
|
|
90
|
+
surface: str,
|
|
91
|
+
target: str,
|
|
92
|
+
message: Optional[str] = None,
|
|
93
|
+
hint: Optional[str] = None,
|
|
94
|
+
) -> DuplicateProblem:
|
|
95
|
+
msg = message if message is not None else f"{surface} declares {target!r} twice"
|
|
96
|
+
return super().__new__(
|
|
97
|
+
cls,
|
|
98
|
+
msg,
|
|
99
|
+
surface=surface,
|
|
100
|
+
code='duplicate',
|
|
101
|
+
target=target,
|
|
102
|
+
hint=hint or 'remove or rename the duplicate declaration',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InvalidIdentifierProblem(Problem):
|
|
107
|
+
"""An identifier name does not follow the required naming pattern."""
|
|
108
|
+
|
|
109
|
+
def __new__(
|
|
110
|
+
cls,
|
|
111
|
+
surface: str,
|
|
112
|
+
target: str,
|
|
113
|
+
message: Optional[str] = None,
|
|
114
|
+
hint: Optional[str] = None,
|
|
115
|
+
) -> InvalidIdentifierProblem:
|
|
116
|
+
msg = message if message is not None else f"{surface} identifier {target!r} is not a valid identifier"
|
|
117
|
+
return super().__new__(
|
|
118
|
+
cls,
|
|
119
|
+
msg,
|
|
120
|
+
surface=surface,
|
|
121
|
+
code='invalid_identifier',
|
|
122
|
+
target=target,
|
|
123
|
+
hint=hint or 'identifiers must start with a letter/underscore and contain only alphanumeric characters',
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ReferenceProblem(Problem):
|
|
128
|
+
"""A declaration references an entity that is unknown or missing."""
|
|
129
|
+
|
|
130
|
+
reference: str
|
|
131
|
+
|
|
132
|
+
def __new__(
|
|
133
|
+
cls,
|
|
134
|
+
surface: str,
|
|
135
|
+
target: str,
|
|
136
|
+
reference: str,
|
|
137
|
+
message: Optional[str] = None,
|
|
138
|
+
hint: Optional[str] = None,
|
|
139
|
+
) -> ReferenceProblem:
|
|
140
|
+
msg = message if message is not None else f"{surface} {target} references unknown {reference!r}"
|
|
141
|
+
obj = super().__new__(
|
|
142
|
+
cls,
|
|
143
|
+
msg,
|
|
144
|
+
surface=surface,
|
|
145
|
+
code='unknown_reference',
|
|
146
|
+
target=target,
|
|
147
|
+
hint=hint,
|
|
148
|
+
)
|
|
149
|
+
obj.reference = str(reference)
|
|
150
|
+
return obj
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> ProblemDict:
|
|
153
|
+
d = super().to_dict()
|
|
154
|
+
d['reference'] = self.reference
|
|
155
|
+
return d
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TypeMismatchProblem(Problem):
|
|
159
|
+
"""A declaration entry has the wrong type or does not implement the expected class."""
|
|
160
|
+
|
|
161
|
+
expected: str
|
|
162
|
+
got: str
|
|
163
|
+
|
|
164
|
+
def __new__(
|
|
165
|
+
cls,
|
|
166
|
+
surface: str,
|
|
167
|
+
expected: str,
|
|
168
|
+
got: str,
|
|
169
|
+
message: Optional[str] = None,
|
|
170
|
+
hint: Optional[str] = None,
|
|
171
|
+
) -> TypeMismatchProblem:
|
|
172
|
+
msg = message if message is not None else f"{surface} holds {expected}, not {got}"
|
|
173
|
+
obj = super().__new__(
|
|
174
|
+
cls,
|
|
175
|
+
msg,
|
|
176
|
+
surface=surface,
|
|
177
|
+
code='type_mismatch',
|
|
178
|
+
target=got,
|
|
179
|
+
hint=hint or f"use instances of {expected}",
|
|
180
|
+
)
|
|
181
|
+
obj.expected = str(expected)
|
|
182
|
+
obj.got = str(got)
|
|
183
|
+
return obj
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> ProblemDict:
|
|
186
|
+
d = super().to_dict()
|
|
187
|
+
d['expected'] = self.expected
|
|
188
|
+
d['got'] = self.got
|
|
189
|
+
return d
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ShadowingProblem(Problem):
|
|
193
|
+
"""A declared key would shadow a method or class attribute."""
|
|
194
|
+
|
|
195
|
+
owner: str
|
|
196
|
+
|
|
197
|
+
def __new__(
|
|
198
|
+
cls,
|
|
199
|
+
surface: str,
|
|
200
|
+
target: str,
|
|
201
|
+
owner: str,
|
|
202
|
+
message: Optional[str] = None,
|
|
203
|
+
hint: Optional[str] = None,
|
|
204
|
+
) -> ShadowingProblem:
|
|
205
|
+
msg = message if message is not None else f"{surface} {target} would shadow {owner}.{target}; pick another key"
|
|
206
|
+
obj = super().__new__(
|
|
207
|
+
cls,
|
|
208
|
+
msg,
|
|
209
|
+
surface=surface,
|
|
210
|
+
code='shadows_attribute',
|
|
211
|
+
target=target,
|
|
212
|
+
hint=hint or 'pick another key to avoid overwriting attributes',
|
|
213
|
+
)
|
|
214
|
+
obj.owner = str(owner)
|
|
215
|
+
return obj
|
|
216
|
+
|
|
217
|
+
def to_dict(self) -> ProblemDict:
|
|
218
|
+
d = super().to_dict()
|
|
219
|
+
d['owner'] = self.owner
|
|
220
|
+
return d
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class IncompatibleProblem(Problem):
|
|
224
|
+
"""Two declarations or declarations and method implementations are incompatible."""
|
|
225
|
+
|
|
226
|
+
def __new__(
|
|
227
|
+
cls,
|
|
228
|
+
surface: str,
|
|
229
|
+
target: str,
|
|
230
|
+
message: str,
|
|
231
|
+
hint: Optional[str] = None,
|
|
232
|
+
) -> IncompatibleProblem:
|
|
233
|
+
return super().__new__(
|
|
234
|
+
cls,
|
|
235
|
+
message,
|
|
236
|
+
surface=surface,
|
|
237
|
+
code='incompatible',
|
|
238
|
+
target=target,
|
|
239
|
+
hint=hint,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class Problems(list[Any]):
|
|
244
|
+
"""A list of Problem instances providing query methods and surface filtering."""
|
|
245
|
+
|
|
246
|
+
def for_surface(self, surface: str) -> list[Problem]:
|
|
247
|
+
"""Return all problems recorded for a specific declaration surface."""
|
|
248
|
+
return [p for p in self if isinstance(p, Problem) and p.surface == surface]
|
|
249
|
+
|
|
250
|
+
def by_code(self, code: str) -> list[Problem]:
|
|
251
|
+
"""Return all problems with a specific semantic code."""
|
|
252
|
+
return [p for p in self if isinstance(p, Problem) and p.code == code]
|
|
253
|
+
|
|
254
|
+
def surfaces(self) -> set[str]:
|
|
255
|
+
"""Set of surfaces that have problems."""
|
|
256
|
+
return {p.surface for p in self if isinstance(p, Problem) and p.surface}
|
|
257
|
+
|
|
258
|
+
def codes(self) -> set[str]:
|
|
259
|
+
"""Set of unique problem codes."""
|
|
260
|
+
return {p.code for p in self if isinstance(p, Problem) and p.code}
|
|
261
|
+
|
|
262
|
+
def to_dicts(self) -> list[ProblemDict]:
|
|
263
|
+
"""Serialize all problems to dictionaries."""
|
|
264
|
+
return [p.to_dict() if isinstance(p, Problem) else {'message': str(p), 'surface': '', 'code': ''} for p in self]
|
|
265
|
+
|
|
266
|
+
def add(
|
|
267
|
+
self,
|
|
268
|
+
problem_or_message: Any,
|
|
269
|
+
*,
|
|
270
|
+
surface: str = '',
|
|
271
|
+
code: str = '',
|
|
272
|
+
target: str = '',
|
|
273
|
+
hint: Optional[str] = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Append a problem or wrap a string message into a Problem."""
|
|
276
|
+
if isinstance(problem_or_message, Problem):
|
|
277
|
+
self.append(problem_or_message)
|
|
278
|
+
else:
|
|
279
|
+
self.append(Problem(str(problem_or_message), surface=surface, code=code, target=target, hint=hint))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---- Runtime Trouble --------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
class TroubleDict(TypedDict, total=False):
|
|
285
|
+
"""Concrete dictionary structure for runtime trouble."""
|
|
286
|
+
|
|
287
|
+
message: str
|
|
288
|
+
code: str
|
|
289
|
+
severity: str
|
|
290
|
+
compartment: str
|
|
291
|
+
since: Optional[int]
|
|
292
|
+
hint: Optional[str]
|
|
293
|
+
metadata: Mapping[str, Any]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class Trouble(str):
|
|
297
|
+
"""What an operator should be told without having to ask for it.
|
|
298
|
+
|
|
299
|
+
Subclasses str so that existing tests asserting against `trouble()`
|
|
300
|
+
strings, substring matches (`'relay' in t`), list equality (`[t] == ['foo']`),
|
|
301
|
+
and string formatting continue to work 100% identically without breaking changes.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
code: str
|
|
305
|
+
compartment: str
|
|
306
|
+
message: str
|
|
307
|
+
severity: str
|
|
308
|
+
since: Optional[int]
|
|
309
|
+
hint: Optional[str]
|
|
310
|
+
metadata: Mapping[str, Any]
|
|
311
|
+
|
|
312
|
+
def __new__(
|
|
313
|
+
cls: type[T_tr],
|
|
314
|
+
message: str,
|
|
315
|
+
*,
|
|
316
|
+
code: str = 'fault',
|
|
317
|
+
compartment: str = '',
|
|
318
|
+
severity: str = 'trouble',
|
|
319
|
+
since: Optional[int] = None,
|
|
320
|
+
hint: Optional[str] = None,
|
|
321
|
+
**metadata: Any,
|
|
322
|
+
) -> T_tr:
|
|
323
|
+
obj = super().__new__(cls, message)
|
|
324
|
+
obj.code = str(code)
|
|
325
|
+
obj.compartment = str(compartment)
|
|
326
|
+
obj.message = str(message)
|
|
327
|
+
obj.severity = str(severity)
|
|
328
|
+
obj.since = since
|
|
329
|
+
obj.hint = hint
|
|
330
|
+
obj.metadata = metadata
|
|
331
|
+
return obj
|
|
332
|
+
|
|
333
|
+
def to_dict(self) -> TroubleDict:
|
|
334
|
+
"""Serialize to a JSON-compatible dictionary."""
|
|
335
|
+
d: TroubleDict = {
|
|
336
|
+
'message': self.message,
|
|
337
|
+
'code': self.code,
|
|
338
|
+
'severity': self.severity,
|
|
339
|
+
}
|
|
340
|
+
if self.compartment:
|
|
341
|
+
d['compartment'] = self.compartment
|
|
342
|
+
if self.since is not None:
|
|
343
|
+
d['since'] = self.since
|
|
344
|
+
if self.hint is not None:
|
|
345
|
+
d['hint'] = self.hint
|
|
346
|
+
if self.metadata:
|
|
347
|
+
d['metadata'] = self.metadata
|
|
348
|
+
return d
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class TroubleList(list[Any]):
|
|
352
|
+
"""A list of Trouble items offering filtering and serialization."""
|
|
353
|
+
|
|
354
|
+
def for_compartment(self, compartment: str) -> list[Trouble]:
|
|
355
|
+
"""Filter troubles belonging to a specific compartment."""
|
|
356
|
+
return [t for t in self if isinstance(t, Trouble) and t.compartment == compartment]
|
|
357
|
+
|
|
358
|
+
def by_code(self, code: str) -> list[Trouble]:
|
|
359
|
+
"""Filter troubles by fault code."""
|
|
360
|
+
return [t for t in self if isinstance(t, Trouble) and t.code == code]
|
|
361
|
+
|
|
362
|
+
def by_severity(self, severity: str) -> list[Trouble]:
|
|
363
|
+
"""Filter troubles by severity ('trouble', 'warning', 'fatal', etc.)."""
|
|
364
|
+
return [t for t in self if isinstance(t, Trouble) and t.severity == severity]
|
|
365
|
+
|
|
366
|
+
def codes(self) -> set[str]:
|
|
367
|
+
"""Set of unique trouble codes."""
|
|
368
|
+
return {t.code for t in self if isinstance(t, Trouble) and t.code}
|
|
369
|
+
|
|
370
|
+
def compartments(self) -> set[str]:
|
|
371
|
+
"""Set of compartments reporting trouble."""
|
|
372
|
+
return {t.compartment for t in self if isinstance(t, Trouble) and t.compartment}
|
|
373
|
+
|
|
374
|
+
def to_dicts(self) -> list[TroubleDict]:
|
|
375
|
+
"""Serialize all troubles to dictionaries."""
|
|
376
|
+
return [t.to_dict() if isinstance(t, Trouble) else {'message': str(t), 'code': 'fault', 'severity': 'trouble'} for t in self]
|
|
377
|
+
|
|
378
|
+
def add(
|
|
379
|
+
self,
|
|
380
|
+
trouble_or_message: Any,
|
|
381
|
+
*,
|
|
382
|
+
code: str = 'fault',
|
|
383
|
+
compartment: str = '',
|
|
384
|
+
severity: str = 'trouble',
|
|
385
|
+
since: Optional[int] = None,
|
|
386
|
+
hint: Optional[str] = None,
|
|
387
|
+
**kw: Any,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Append a trouble item or wrap a raw string into a Trouble instance."""
|
|
390
|
+
if isinstance(trouble_or_message, Trouble):
|
|
391
|
+
self.append(trouble_or_message)
|
|
392
|
+
else:
|
|
393
|
+
self.append(Trouble(
|
|
394
|
+
str(trouble_or_message),
|
|
395
|
+
code=code,
|
|
396
|
+
compartment=compartment,
|
|
397
|
+
severity=severity,
|
|
398
|
+
since=since,
|
|
399
|
+
hint=hint,
|
|
400
|
+
**kw,
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ---- Runtime Notes ----------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
T_note = TypeVar('T_note', bound='Note')
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class NoteDict(TypedDict, total=False):
|
|
410
|
+
"""Concrete dictionary structure for runtime notes."""
|
|
411
|
+
|
|
412
|
+
message: str
|
|
413
|
+
topic: str
|
|
414
|
+
level: str
|
|
415
|
+
compartment: str
|
|
416
|
+
since: Optional[int]
|
|
417
|
+
metadata: Mapping[str, Any]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class Note(str):
|
|
421
|
+
"""What an operator would want to read, that is not a fault.
|
|
422
|
+
|
|
423
|
+
A relay cooling after a rate-limit, a peer that announced it is
|
|
424
|
+
leaving: the node behaving, and the reason a quiet mesh is quiet.
|
|
425
|
+
Kept apart from `trouble()` so no reader has to classify prose.
|
|
426
|
+
Subclasses str so that existing tests asserting against `notes()`
|
|
427
|
+
strings, substring matches, or list equality continue to work 100% identically.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
topic: str
|
|
431
|
+
compartment: str
|
|
432
|
+
message: str
|
|
433
|
+
level: str
|
|
434
|
+
since: Optional[int]
|
|
435
|
+
metadata: Mapping[str, Any]
|
|
436
|
+
|
|
437
|
+
def __new__(
|
|
438
|
+
cls: type[T_note],
|
|
439
|
+
message: str,
|
|
440
|
+
*,
|
|
441
|
+
topic: str = 'general',
|
|
442
|
+
compartment: str = '',
|
|
443
|
+
level: str = 'info',
|
|
444
|
+
since: Optional[int] = None,
|
|
445
|
+
**metadata: Any,
|
|
446
|
+
) -> T_note:
|
|
447
|
+
obj = super().__new__(cls, message)
|
|
448
|
+
obj.topic = str(topic)
|
|
449
|
+
obj.compartment = str(compartment)
|
|
450
|
+
obj.message = str(message)
|
|
451
|
+
obj.level = str(level)
|
|
452
|
+
obj.since = since
|
|
453
|
+
obj.metadata = metadata
|
|
454
|
+
return obj
|
|
455
|
+
|
|
456
|
+
def to_dict(self) -> NoteDict:
|
|
457
|
+
"""Serialize to a JSON-compatible dictionary."""
|
|
458
|
+
d: NoteDict = {
|
|
459
|
+
'message': self.message,
|
|
460
|
+
'topic': self.topic,
|
|
461
|
+
'level': self.level,
|
|
462
|
+
}
|
|
463
|
+
if self.compartment:
|
|
464
|
+
d['compartment'] = self.compartment
|
|
465
|
+
if self.since is not None:
|
|
466
|
+
d['since'] = self.since
|
|
467
|
+
if self.metadata:
|
|
468
|
+
d['metadata'] = self.metadata
|
|
469
|
+
return d
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class NoteList(list[Any]):
|
|
473
|
+
"""A list of Note items offering filtering and serialization."""
|
|
474
|
+
|
|
475
|
+
def for_compartment(self, compartment: str) -> list[Note]:
|
|
476
|
+
"""Filter notes belonging to a specific compartment."""
|
|
477
|
+
return [n for n in self if isinstance(n, Note) and n.compartment == compartment]
|
|
478
|
+
|
|
479
|
+
def by_topic(self, topic: str) -> list[Note]:
|
|
480
|
+
"""Filter notes by topic."""
|
|
481
|
+
return [n for n in self if isinstance(n, Note) and n.topic == topic]
|
|
482
|
+
|
|
483
|
+
def by_level(self, level: str) -> list[Note]:
|
|
484
|
+
"""Filter notes by level ('info', 'notice', 'debug', etc.)."""
|
|
485
|
+
return [n for n in self if isinstance(n, Note) and n.level == level]
|
|
486
|
+
|
|
487
|
+
def topics(self) -> set[str]:
|
|
488
|
+
"""Set of unique note topics."""
|
|
489
|
+
return {n.topic for n in self if isinstance(n, Note) and n.topic}
|
|
490
|
+
|
|
491
|
+
def compartments(self) -> set[str]:
|
|
492
|
+
"""Set of compartments reporting notes."""
|
|
493
|
+
return {n.compartment for n in self if isinstance(n, Note) and n.compartment}
|
|
494
|
+
|
|
495
|
+
def to_dicts(self) -> list[NoteDict]:
|
|
496
|
+
"""Serialize all notes to dictionaries."""
|
|
497
|
+
return [n.to_dict() if isinstance(n, Note) else {'message': str(n), 'topic': 'general', 'level': 'info'} for n in self]
|
|
498
|
+
|
|
499
|
+
def add(
|
|
500
|
+
self,
|
|
501
|
+
note_or_message: Any,
|
|
502
|
+
*,
|
|
503
|
+
topic: str = 'general',
|
|
504
|
+
compartment: str = '',
|
|
505
|
+
level: str = 'info',
|
|
506
|
+
since: Optional[int] = None,
|
|
507
|
+
**kw: Any,
|
|
508
|
+
) -> None:
|
|
509
|
+
"""Append a note item or wrap a raw string into a Note instance."""
|
|
510
|
+
if isinstance(note_or_message, Note):
|
|
511
|
+
self.append(note_or_message)
|
|
512
|
+
else:
|
|
513
|
+
self.append(Note(
|
|
514
|
+
str(note_or_message),
|
|
515
|
+
topic=topic,
|
|
516
|
+
compartment=compartment,
|
|
517
|
+
level=level,
|
|
518
|
+
since=since,
|
|
519
|
+
**kw,
|
|
520
|
+
))
|
|
521
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Every 'no' a compartment says, counted by reason and kept with detail.
|
|
2
|
+
|
|
3
|
+
Split out of node/compartment.py when it became a package; see that package's
|
|
4
|
+
docstring for why a refusal that is not recorded is indistinguishable from
|
|
5
|
+
silence.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import collections
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Callable, Counter, Deque, Mapping, NamedTuple, Optional, TypedDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Refusal(NamedTuple):
|
|
16
|
+
at: int
|
|
17
|
+
kind: str
|
|
18
|
+
subject: Optional[str]
|
|
19
|
+
why: str
|
|
20
|
+
detail: Mapping[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RefusalRecentSummary(TypedDict):
|
|
24
|
+
at: int
|
|
25
|
+
kind: str
|
|
26
|
+
subject: Optional[str]
|
|
27
|
+
why: str
|
|
28
|
+
detail: Mapping[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RefusalsStateReport(TypedDict):
|
|
32
|
+
total: int
|
|
33
|
+
by_reason: dict[str, int]
|
|
34
|
+
recent: list[RefusalRecentSummary]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Refusals:
|
|
38
|
+
"""Every 'no' one compartment said, counted by reason and kept with detail.
|
|
39
|
+
|
|
40
|
+
Counted so a rate is visible; kept so the last few can be read. A reason is
|
|
41
|
+
a short stable string and never a formatted id, because a reason is a
|
|
42
|
+
category and the ids belong in the detail where they cannot explode the
|
|
43
|
+
key space.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: Optional[str], keep: int = 200,
|
|
47
|
+
clock: Optional[Callable[[], int]] = None) -> None:
|
|
48
|
+
self.name: Optional[str] = name
|
|
49
|
+
self.keep: int = keep
|
|
50
|
+
# Stamps and windows read this clock when a caller gives no `at`/`now`.
|
|
51
|
+
# A compartment hands its own, so a refusal and the trouble() that
|
|
52
|
+
# reads it never disagree about what "now" is.
|
|
53
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
54
|
+
self._lock: threading.Lock = threading.Lock()
|
|
55
|
+
self.counts: Counter[str] = collections.Counter()
|
|
56
|
+
self.recent: Deque[Refusal] = collections.deque(maxlen=keep)
|
|
57
|
+
|
|
58
|
+
def refuse(
|
|
59
|
+
self,
|
|
60
|
+
kind: str,
|
|
61
|
+
why: str,
|
|
62
|
+
subject: Optional[str] = None,
|
|
63
|
+
detail: Optional[Mapping[str, Any]] = None,
|
|
64
|
+
at: Optional[int] = None,
|
|
65
|
+
) -> Refusal:
|
|
66
|
+
r = Refusal(int(at if at is not None else self.clock()), kind, subject, why, detail or {})
|
|
67
|
+
with self._lock:
|
|
68
|
+
self.counts['%s: %s' % (kind, why)] += 1
|
|
69
|
+
self.recent.append(r)
|
|
70
|
+
return r
|
|
71
|
+
|
|
72
|
+
def total(self) -> int:
|
|
73
|
+
return sum(self.counts.values())
|
|
74
|
+
|
|
75
|
+
def last(self, n: int = 5) -> list[Refusal]:
|
|
76
|
+
with self._lock:
|
|
77
|
+
return list(self.recent)[-n:]
|
|
78
|
+
|
|
79
|
+
def within(
|
|
80
|
+
self,
|
|
81
|
+
seconds: int,
|
|
82
|
+
now: Optional[int] = None,
|
|
83
|
+
kind: Optional[str] = None,
|
|
84
|
+
why: Optional[str] = None,
|
|
85
|
+
) -> list[Refusal]:
|
|
86
|
+
"""Refusals from the last `seconds`, which is what trouble means.
|
|
87
|
+
|
|
88
|
+
Trouble is what is wrong NOW. Reading the whole buffer reports a fault
|
|
89
|
+
that has been fixed until the buffer happens to roll, and an operator
|
|
90
|
+
who is shouted at about something already mended learns to stop reading
|
|
91
|
+
the shouting. The counts keep the history; this answers the present.
|
|
92
|
+
"""
|
|
93
|
+
cut = int(now if now is not None else self.clock()) - seconds
|
|
94
|
+
with self._lock:
|
|
95
|
+
rows = [r for r in self.recent if r.at >= cut]
|
|
96
|
+
if kind is not None:
|
|
97
|
+
rows = [r for r in rows if r.kind == kind]
|
|
98
|
+
if why is not None:
|
|
99
|
+
rows = [r for r in rows if r.why == why]
|
|
100
|
+
return rows
|
|
101
|
+
|
|
102
|
+
def state(self, n: int = 5) -> RefusalsStateReport:
|
|
103
|
+
return {
|
|
104
|
+
'total': self.total(),
|
|
105
|
+
'by_reason': dict(self.counts),
|
|
106
|
+
'recent': [
|
|
107
|
+
{
|
|
108
|
+
'at': r.at,
|
|
109
|
+
'kind': r.kind,
|
|
110
|
+
'subject': (r.subject or '')[:16] or None,
|
|
111
|
+
'why': r.why,
|
|
112
|
+
'detail': r.detail,
|
|
113
|
+
}
|
|
114
|
+
for r in self.last(n)
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def why_detail(e: BaseException) -> dict[str, str]:
|
|
120
|
+
return {'error': '%s: %s' % (type(e).__name__, e)}
|