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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. 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)}