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,108 @@
1
+ """EVERY: periodic work declared on a compartment."""
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import math
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable, Optional, Union
8
+
9
+ from .problem import Problem, Problems, ReferenceProblem, TypeMismatchProblem
10
+
11
+ SecondsType = Union[int, float, Callable[[Any], Union[int, float]], Any]
12
+ ReportType = Optional[Union[str, Callable[..., Any]]]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Every:
17
+ """A periodic job declared on a compartment class.
18
+
19
+ Attributes:
20
+ seconds: How often to run the job, in seconds. Can be an int, float,
21
+ a callable receiving the compartment instance, or a Parameter.
22
+ A callable that resolves to zero or less turns the job off
23
+ (see `is_off`): that is how a setting like `check_every: 0`
24
+ says "only on command".
25
+ run: The method name on the compartment to call when due.
26
+ name: A unique name for the job within the compartment (defaults to `run`).
27
+ report: Optional method name or callable to convert the method's return value
28
+ into a list of `[(what, ...)]` tuples for the tick log.
29
+ """
30
+ seconds: SecondsType
31
+ run: str
32
+ name: str = ''
33
+ report: ReportType = None
34
+ # Whether `run` takes `now`, per class. inspect.signature ran for every
35
+ # due job on every tick (audit 2026-10-15, E6); a method's arity does not
36
+ # change between ticks.
37
+ _takes_now: dict = field(default_factory=dict, init=False, repr=False, compare=False, hash=False)
38
+
39
+ def __post_init__(self) -> None:
40
+ if not self.name:
41
+ object.__setattr__(self, 'name', self.run)
42
+
43
+ def get_seconds(self, instance: Any) -> float:
44
+ """Resolve the interval in seconds against a compartment instance."""
45
+ sec = self.seconds
46
+ if callable(sec):
47
+ return float(sec(instance))
48
+ if hasattr(sec, 'value') and callable(sec.value):
49
+ return float(sec.value())
50
+ if hasattr(sec, 'default'):
51
+ def_val = sec.default() if callable(sec.default) else sec.default
52
+ return float(def_val)
53
+ return float(sec)
54
+
55
+ def is_off(self, instance: Any) -> bool:
56
+ """Whether the interval says this job does not run on a schedule.
57
+
58
+ Zero, a negative number, or anything not finite. Updates used to spell
59
+ "off" as infinity, and `report()` died on `int(inf)` - which took the
60
+ whole health report with it, every tick (audit 2026-10-15, B1). Off is
61
+ now one rule, read here, and never a number anyone does arithmetic on.
62
+ """
63
+ sec = self.get_seconds(instance)
64
+ return not math.isfinite(sec) or sec <= 0
65
+
66
+ def due(self, instance: Any, now: int) -> bool:
67
+ """True if this job has never run or if at least `seconds` have passed since last run.
68
+ Never true for a job that is off, including on its first tick."""
69
+ if self.is_off(instance):
70
+ return False
71
+ every_last = getattr(instance, '_every_last', None)
72
+ if every_last is None:
73
+ return True
74
+ last = every_last.get(self.name)
75
+ if last is None:
76
+ return True
77
+ return (now - last) >= self.get_seconds(instance)
78
+
79
+ def execute(self, instance: Any, now: int) -> Any:
80
+ """Call the target method on instance, passing `now` if accepted."""
81
+ method = getattr(instance, self.run, None)
82
+ if method is None:
83
+ raise AttributeError(f"{type(instance).__name__} has no method {self.run!r}")
84
+ cls = type(instance)
85
+ takes = self._takes_now.get(cls)
86
+ if takes is None:
87
+ takes = self._takes_now[cls] = len(inspect.signature(method).parameters) > 0
88
+ if takes:
89
+ return method(now)
90
+ return method()
91
+
92
+ def problems(self, cls: type[Any]) -> list[str]:
93
+ """Lint this declaration against the target class."""
94
+ out = Problems()
95
+ if not isinstance(self.run, str) or not self.run:
96
+ out.append(TypeMismatchProblem('EVERY', 'non-empty str', type(self.run).__name__,
97
+ message=f"Every job {self.name!r}: run must be a non-empty string method name"))
98
+ elif not getattr(cls, 'abstract', False) and not hasattr(cls, self.run):
99
+ out.append(ReferenceProblem('EVERY', f"Every job {self.name!r}", self.run,
100
+ message=f"Every job {self.name!r}: method {self.run!r} does not exist on {cls.__name__}"))
101
+ if isinstance(self.report, str) and not getattr(cls, 'abstract', False) and not hasattr(cls, self.report):
102
+ out.append(ReferenceProblem('EVERY', f"Every job {self.name!r}", self.report,
103
+ message=f"Every job {self.name!r}: report method {self.report!r} does not exist on {cls.__name__}"))
104
+ if isinstance(self.seconds, (int, float)) and self.seconds <= 0:
105
+ out.append(Problem(f"Every job {self.name!r}: seconds must be positive, got {self.seconds!r}",
106
+ surface='EVERY', code='invalid_interval', target=self.name))
107
+ return out
108
+
@@ -0,0 +1,131 @@
1
+ """HANDLERS: what a compartment can receive from the wire, declared where it is.
2
+
3
+ A wire compartment declares its message dispatch table as a tuple of typed
4
+ Handler entries on the class attribute HANDLERS. Like COMMANDS, WORKERS, and
5
+ CHANNELS, it is declared once, validated at class definition time by the base
6
+ Compartment, and dispatched uniformly by the base `handle(inbound)`.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import inspect
12
+ from typing import Any, Callable, Optional, Sequence
13
+
14
+ from .problem import (
15
+ InvalidIdentifierProblem,
16
+ Problem,
17
+ Problems,
18
+ ReferenceProblem,
19
+ TypeMismatchProblem,
20
+ )
21
+
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+ class Handler:
26
+ """One inbound wire protocol message handler."""
27
+
28
+ kind: str
29
+ run: str
30
+ schema: Optional[Any] = None
31
+ doc: str = ''
32
+ exact: bool = False
33
+ reads: tuple[str, ...] = ()
34
+ writes: tuple[str, ...] = ()
35
+
36
+ def __init__(
37
+ self,
38
+ kind: str,
39
+ run: str | Callable[..., Any],
40
+ *,
41
+ schema: Optional[Any] = None,
42
+ doc: str = '',
43
+ exact: bool = False,
44
+ reads: Sequence[str] = (),
45
+ writes: Sequence[str] = (),
46
+ ) -> None:
47
+ object.__setattr__(self, 'kind', str(kind))
48
+ run_name = run if isinstance(run, str) else getattr(run, '__name__', str(run))
49
+ object.__setattr__(self, 'run', run_name)
50
+ object.__setattr__(self, 'schema', schema)
51
+ object.__setattr__(self, 'doc', str(doc))
52
+ is_exact = exact or (kind.startswith(('http://', 'https://')) and not exact)
53
+ object.__setattr__(self, 'exact', bool(is_exact))
54
+ object.__setattr__(self, 'reads', tuple(reads))
55
+ object.__setattr__(self, 'writes', tuple(writes))
56
+
57
+ def matches(self, msg_type: str, piuris: Sequence[str] = ()) -> bool:
58
+ """Check if this handler matches the given message type URI."""
59
+ if not msg_type:
60
+ return False
61
+ if self.kind == '*':
62
+ if not piuris:
63
+ return True
64
+ return any(msg_type.startswith(p + '/') for p in piuris)
65
+ if self.exact or self.kind.startswith(('http://', 'https://')):
66
+ return msg_type == self.kind
67
+ # Suffix / sub-path matching: https://fleetmesh.org/compute/1.0/quote
68
+ if msg_type.endswith('/' + self.kind):
69
+ if not piuris:
70
+ return True
71
+ prefix = msg_type.rsplit('/', 1)[0]
72
+ return any(prefix == p or prefix.startswith(p) for p in piuris)
73
+ return False
74
+
75
+ def problems(self, cls: type) -> list[str]:
76
+ """Validate this Handler against the declaring compartment class."""
77
+ out = Problems()
78
+ if not self.kind or not isinstance(self.kind, str):
79
+ out.append(InvalidIdentifierProblem('HANDLERS', str(self.kind), 'Handler must specify a message kind'))
80
+ if not self.run or not isinstance(self.run, str):
81
+ out.append(Problem(
82
+ f'Handler {self.kind} must specify a target run method',
83
+ surface='HANDLERS',
84
+ code='missing_run',
85
+ target=self.kind,
86
+ ))
87
+ else:
88
+ fn = getattr(cls, self.run, None)
89
+ if fn is None:
90
+ out.append(ReferenceProblem(
91
+ 'HANDLERS', f'{cls.__name__}.{self.run}', self.run,
92
+ f"handler {self.kind!r}: method {self.run!r} does not exist on {cls.__name__}"
93
+ ))
94
+ elif not callable(fn):
95
+ out.append(TypeMismatchProblem(
96
+ 'HANDLERS', 'callable', type(fn).__name__,
97
+ f"handler {self.kind!r}: attribute {self.run!r} on {cls.__name__} is not callable"
98
+ ))
99
+ return out
100
+
101
+ def execute(self, instance: Any, inbound: Any, *args: Any, **kw: Any) -> Any:
102
+ """Dispatch an inbound message to this handler on the compartment instance."""
103
+ fn = getattr(instance, self.run)
104
+ sig = inspect.signature(fn)
105
+ num_params = len(sig.parameters)
106
+ msg = getattr(inbound, 'message', inbound)
107
+ sender = getattr(inbound, 'sender', msg.get('from', '') if isinstance(msg, dict) else '')
108
+
109
+ # Validate against schema if declared
110
+ if self.schema is not None and isinstance(msg, dict):
111
+ body = msg.get('body')
112
+ if callable(self.schema):
113
+ try:
114
+ self.schema(body)
115
+ except Exception as e:
116
+ return ('refused', sender, f'invalid schema: {e}')
117
+
118
+ available = (inbound, sender) + args
119
+ if num_params == 0:
120
+ return fn()
121
+ elif num_params == 1:
122
+ return fn(inbound)
123
+ elif num_params == 2:
124
+ return fn(inbound, sender)
125
+ elif num_params <= len(available):
126
+ return fn(*available[:num_params], **kw)
127
+ else:
128
+ return fn(inbound, sender)
129
+
130
+
131
+ __all__ = ['Handler']