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,458 @@
|
|
|
1
|
+
"""COMMANDS: what a compartment can be asked to do, declared where it is.
|
|
2
|
+
|
|
3
|
+
Split out of node/compartment.py when it became a package. node/commands
|
|
4
|
+
re-exports these for callers that think in commands rather than compartments.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any, Callable, Mapping, Optional, Sequence, TypedDict, Union
|
|
11
|
+
|
|
12
|
+
from ..config.parameter import Parameter, ParameterRecord
|
|
13
|
+
from ..interfaces import CommandSpec
|
|
14
|
+
from .problem import (DuplicateProblem, Problem, Problems, ReferenceProblem,
|
|
15
|
+
TypeMismatchProblem)
|
|
16
|
+
|
|
17
|
+
ParameterValue = Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CommandRecord(TypedDict, total=False):
|
|
21
|
+
"""A command as `meshnode commands`, control socket, and docs read it."""
|
|
22
|
+
key: str
|
|
23
|
+
name: str
|
|
24
|
+
description: str
|
|
25
|
+
tags: list[str]
|
|
26
|
+
parameters: list[ParameterRecord]
|
|
27
|
+
permission: str
|
|
28
|
+
permission_enforced: bool
|
|
29
|
+
process: list[str]
|
|
30
|
+
process_enforced: bool
|
|
31
|
+
reply: str
|
|
32
|
+
reads: list[str]
|
|
33
|
+
writes: list[str]
|
|
34
|
+
compartment: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CommandError(ValueError):
|
|
38
|
+
"""The caller asked wrongly: an argument missing, unknown, or not what
|
|
39
|
+
the command takes. A refusal the caller reads, never a fault in the
|
|
40
|
+
node - the control socket answers it as one (`ControlError`) and keeps
|
|
41
|
+
it out of the error ring that counts bugs."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass(frozen=True)
|
|
45
|
+
class Command:
|
|
46
|
+
"""One operation a compartment exposes, declared where the compartment is.
|
|
47
|
+
|
|
48
|
+
Declared once, in the compartment's COMMANDS, and answered by the base:
|
|
49
|
+
`Compartment.command` finds the declaration, checks the arguments against
|
|
50
|
+
it, and calls what `run` names. A compartment that declares COMMANDS does
|
|
51
|
+
not write its own `command()` - the if-chain that used to sit beside a
|
|
52
|
+
hand-kept `commands` dict was the second copy of every declaration.
|
|
53
|
+
|
|
54
|
+
Fields:
|
|
55
|
+
key: short name of the command on the compartment (e.g. 'show', 'test')
|
|
56
|
+
name: human-readable title (e.g. 'Show announce status')
|
|
57
|
+
description: full prose explanation of what it does
|
|
58
|
+
tags: categorization tags (e.g. ('announce', 'status'))
|
|
59
|
+
parameters: the arguments it takes, as Parameters. Nothing else is
|
|
60
|
+
accepted: an argument it does not declare is refused by name, so a
|
|
61
|
+
misspelt optional one is an error rather than silently its default.
|
|
62
|
+
permission: UCAN ability required (e.g. 'announce/show') - declared,
|
|
63
|
+
not yet enforced (see `describe`)
|
|
64
|
+
process: sequence of required processes / sidecars (e.g. ('sidecar',))
|
|
65
|
+
run: what answers it. The name of a method on the compartment, taking
|
|
66
|
+
either nothing or the checked arguments as one dict; or a callable
|
|
67
|
+
`(compartment, args)`. Checked when the compartment class is defined
|
|
68
|
+
(`problems`), so a name that does not resolve never reaches a node.
|
|
69
|
+
reply: description of return structure
|
|
70
|
+
reads / writes: the stores it reads and writes - this compartment's by
|
|
71
|
+
key, another's as `name.key` (one it requires or uses). Checked when
|
|
72
|
+
the compartment class is defined; the census reads them to say what
|
|
73
|
+
data each command touches.
|
|
74
|
+
"""
|
|
75
|
+
key: str
|
|
76
|
+
name: str
|
|
77
|
+
description: str
|
|
78
|
+
tags: tuple[str, ...] = ()
|
|
79
|
+
parameters: tuple[Parameter[Any], ...] = ()
|
|
80
|
+
permission: str = ''
|
|
81
|
+
process: tuple[str, ...] = ()
|
|
82
|
+
run: Optional[Union[Callable[..., Any], str]] = None
|
|
83
|
+
reply: str = ''
|
|
84
|
+
reads: tuple[str, ...] = ()
|
|
85
|
+
writes: tuple[str, ...] = ()
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
key: str,
|
|
90
|
+
name: str,
|
|
91
|
+
description: str,
|
|
92
|
+
tags: Sequence[str] = (),
|
|
93
|
+
parameters: Sequence[Parameter[Any]] = (),
|
|
94
|
+
permission: str = '',
|
|
95
|
+
process: Sequence[str] | str = (),
|
|
96
|
+
run: Optional[Union[Callable[..., Any], str]] = None,
|
|
97
|
+
reply: str = '',
|
|
98
|
+
*,
|
|
99
|
+
reads: Sequence[str] = (),
|
|
100
|
+
writes: Sequence[str] = (),
|
|
101
|
+
) -> None:
|
|
102
|
+
object.__setattr__(self, 'key', key)
|
|
103
|
+
object.__setattr__(self, 'name', name)
|
|
104
|
+
object.__setattr__(self, 'description', description)
|
|
105
|
+
object.__setattr__(self, 'tags', tuple(tags))
|
|
106
|
+
object.__setattr__(self, 'parameters', tuple(parameters))
|
|
107
|
+
object.__setattr__(self, 'permission', permission)
|
|
108
|
+
procs = (process,) if isinstance(process, str) else tuple(process)
|
|
109
|
+
object.__setattr__(self, 'process', tuple(p for p in procs if p))
|
|
110
|
+
object.__setattr__(self, 'run', run)
|
|
111
|
+
object.__setattr__(self, 'reply', reply)
|
|
112
|
+
object.__setattr__(self, 'reads', tuple(reads))
|
|
113
|
+
object.__setattr__(self, 'writes', tuple(writes))
|
|
114
|
+
|
|
115
|
+
def resource(self, subsystem: str, node_did: str = '') -> str:
|
|
116
|
+
"""The UCAN resource this command is, as `with` would name it.
|
|
117
|
+
|
|
118
|
+
Matches `Parameter.resource()` convention:
|
|
119
|
+
`command/<subsystem>.<key>`, prefixed with node_did if present.
|
|
120
|
+
"""
|
|
121
|
+
path = 'command/%s.%s' % (subsystem, self.key)
|
|
122
|
+
return '%s#%s' % (node_did, path) if node_did else path
|
|
123
|
+
|
|
124
|
+
# ---- checked when the compartment class is defined ----------------------
|
|
125
|
+
def problems(self, cls: type) -> list[str]:
|
|
126
|
+
"""Everything wrong with this declaration on compartment `cls`, as
|
|
127
|
+
sentences - collected by `Compartment.declaration_problems`, so a
|
|
128
|
+
command that cannot be answered fails at import, not on first use."""
|
|
129
|
+
out = Problems()
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
# The annotations promise Parameters and a str-or-callable run; this
|
|
132
|
+
# is for the declaration that broke the promise, so look at what it
|
|
133
|
+
# actually holds.
|
|
134
|
+
params: tuple[object, ...] = tuple(self.parameters)
|
|
135
|
+
run: object = self.run
|
|
136
|
+
for p in params:
|
|
137
|
+
if not isinstance(p, Parameter):
|
|
138
|
+
out.append(TypeMismatchProblem('COMMANDS', 'node.config.Parameter', type(p).__name__,
|
|
139
|
+
message='command %s: parameters are node.config.Parameter, not %s' % (self.key, type(p).__name__)))
|
|
140
|
+
continue
|
|
141
|
+
if p.key in seen:
|
|
142
|
+
out.append(DuplicateProblem('COMMANDS', p.key, message='command %s declares parameter %r twice' % (self.key, p.key)))
|
|
143
|
+
seen.add(p.key)
|
|
144
|
+
if run is None:
|
|
145
|
+
out.append(Problem('command %s has no run: name the method that answers it' % self.key, surface='COMMANDS', code='missing_run', target=self.key))
|
|
146
|
+
elif isinstance(run, str):
|
|
147
|
+
fn = getattr(cls, run, None)
|
|
148
|
+
if fn is None or not callable(fn):
|
|
149
|
+
out.append(ReferenceProblem('COMMANDS', f"command {self.key}", run,
|
|
150
|
+
message='command %s runs %r, which %s has no method called' % (self.key, run, cls.__name__)))
|
|
151
|
+
elif _takes(fn, bound=False) is None:
|
|
152
|
+
out.append(Problem('command %s runs %s.%s, which must take nothing or one argument (the args)' % (self.key, cls.__name__, run),
|
|
153
|
+
surface='COMMANDS', code='invalid_signature', target=self.key))
|
|
154
|
+
elif not callable(run):
|
|
155
|
+
out.append(TypeMismatchProblem('COMMANDS', 'method name or a callable', type(run).__name__,
|
|
156
|
+
message='command %s: run is a method name or a callable, not %s' % (self.key, type(run).__name__)))
|
|
157
|
+
else:
|
|
158
|
+
try:
|
|
159
|
+
inspect.signature(run).bind(None, None)
|
|
160
|
+
except TypeError:
|
|
161
|
+
out.append(Problem('command %s: a callable run takes (compartment, args)' % self.key,
|
|
162
|
+
surface='COMMANDS', code='invalid_signature', target=self.key))
|
|
163
|
+
except ValueError:
|
|
164
|
+
pass # a builtin with no signature to read; called as declared
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---- answered -----------------------------------------------------------
|
|
169
|
+
def validate_args(self, args: Optional[Mapping[str, Any]] = None) -> Mapping[str, ParameterValue]:
|
|
170
|
+
"""The arguments, checked against what this command declares.
|
|
171
|
+
|
|
172
|
+
Every declared one is present in what comes back - the caller's
|
|
173
|
+
value coerced and checked, or the default. A required one missing,
|
|
174
|
+
a value that is not what the parameter states, or an argument the
|
|
175
|
+
command does not take at all is a CommandError naming it.
|
|
176
|
+
"""
|
|
177
|
+
raw = dict(args or {})
|
|
178
|
+
declared = {p.key for p in self.parameters}
|
|
179
|
+
unknown = sorted(k for k in raw if k not in declared)
|
|
180
|
+
if unknown:
|
|
181
|
+
raise CommandError('%s takes no argument %s; it takes %s'
|
|
182
|
+
% (self.key, ', '.join(map(repr, unknown)),
|
|
183
|
+
', '.join(sorted(declared)) or 'none'))
|
|
184
|
+
resolved: dict[str, ParameterValue] = {}
|
|
185
|
+
for p in self.parameters:
|
|
186
|
+
v = raw.get(p.key)
|
|
187
|
+
if v is None:
|
|
188
|
+
if p.required:
|
|
189
|
+
raise CommandError('%s needs %s' % (self.key, p.key))
|
|
190
|
+
resolved[p.key] = p.value()
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
v = _coerced(p, v)
|
|
194
|
+
except (TypeError, ValueError) as e:
|
|
195
|
+
raise CommandError('%s: %s %s' % (self.key, p.key, _must(p, e))) from e
|
|
196
|
+
if p.validate is None and not p.ok(v):
|
|
197
|
+
raise CommandError('%s: %s %s, not %r' % (self.key, p.key, _must(p), str(v)[:32]))
|
|
198
|
+
resolved[p.key] = v
|
|
199
|
+
return resolved
|
|
200
|
+
|
|
201
|
+
def execute(self, target: Any, args: Optional[Mapping[str, Any]] = None) -> Any:
|
|
202
|
+
"""Answer this command on `target` (the compartment instance)."""
|
|
203
|
+
resolved = self.validate_args(args)
|
|
204
|
+
# If target has a check_command_dependencies hook, verify them
|
|
205
|
+
if hasattr(target, 'check_command_dependencies'):
|
|
206
|
+
target.check_command_dependencies(self)
|
|
207
|
+
if isinstance(self.run, str):
|
|
208
|
+
fn = getattr(target, self.run)
|
|
209
|
+
return fn(resolved) if _takes(fn, bound=True) else fn()
|
|
210
|
+
if callable(self.run):
|
|
211
|
+
return self.run(target, resolved)
|
|
212
|
+
raise TypeError('command %s has no run' % self.key)
|
|
213
|
+
|
|
214
|
+
def to_spec(self, subsystem: str = '') -> CommandSpec:
|
|
215
|
+
"""Convert to legacy CommandSpec dictionary for backward compatibility."""
|
|
216
|
+
args_spec: dict[str, str] = {}
|
|
217
|
+
params: tuple[object, ...] = tuple(self.parameters)
|
|
218
|
+
for p in params:
|
|
219
|
+
# Derived at class creation, before the lint: a non-Parameter is
|
|
220
|
+
# left for `problems()` to name rather than crashing here.
|
|
221
|
+
if not isinstance(p, Parameter):
|
|
222
|
+
continue
|
|
223
|
+
if getattr(p, 'required', False):
|
|
224
|
+
args_spec[p.key] = 'required'
|
|
225
|
+
elif getattr(p, 'choices', None):
|
|
226
|
+
args_spec[p.key] = ' | '.join(str(c) for c in p.choices)
|
|
227
|
+
else:
|
|
228
|
+
args_spec[p.key] = 'optional'
|
|
229
|
+
spec: CommandSpec = {
|
|
230
|
+
'args': args_spec,
|
|
231
|
+
'does': self.description,
|
|
232
|
+
'reply': self.reply,
|
|
233
|
+
}
|
|
234
|
+
if subsystem:
|
|
235
|
+
spec['subsystem'] = subsystem
|
|
236
|
+
return spec
|
|
237
|
+
|
|
238
|
+
def describe(self) -> CommandRecord:
|
|
239
|
+
out: CommandRecord = {
|
|
240
|
+
'key': self.key,
|
|
241
|
+
'name': self.name,
|
|
242
|
+
'description': self.description,
|
|
243
|
+
'tags': list(self.tags),
|
|
244
|
+
'permission': self.permission,
|
|
245
|
+
'process': list(self.process),
|
|
246
|
+
}
|
|
247
|
+
# A declared ability must never read as an enforced one. `execute`
|
|
248
|
+
# validates arguments and asks the target to check dependencies; it
|
|
249
|
+
# does not check `permission`, and nothing authenticates a principal
|
|
250
|
+
# yet - the control socket is chmod 0o600 with no peer credential and
|
|
251
|
+
# no token. This is the same footing, and the same reason, as
|
|
252
|
+
# `Parameter.requires` and `config.change.ACCESS_DECLARED_ONLY`, and
|
|
253
|
+
# `requires_enforced: False` on a parameter row. Stated on the row
|
|
254
|
+
# because a row is what an operator reads.
|
|
255
|
+
if self.permission:
|
|
256
|
+
out['permission_enforced'] = False
|
|
257
|
+
# The dependency check is advisory in the same way: `execute` calls
|
|
258
|
+
# `check_command_dependencies` only when the target happens to have
|
|
259
|
+
# one, so a compartment without the hook declares its sidecars and
|
|
260
|
+
# nothing verifies them.
|
|
261
|
+
if self.process:
|
|
262
|
+
out['process_enforced'] = False
|
|
263
|
+
if self.reply:
|
|
264
|
+
out['reply'] = self.reply
|
|
265
|
+
if self.reads:
|
|
266
|
+
out['reads'] = list(self.reads)
|
|
267
|
+
if self.writes:
|
|
268
|
+
out['writes'] = list(self.writes)
|
|
269
|
+
if self.parameters:
|
|
270
|
+
out['parameters'] = [
|
|
271
|
+
{
|
|
272
|
+
'key': p.key,
|
|
273
|
+
'name': getattr(p, 'name', p.key),
|
|
274
|
+
'description': getattr(p, 'description', ''),
|
|
275
|
+
'type': getattr(getattr(p, 'type', object), '__name__', str(getattr(p, 'type', ''))),
|
|
276
|
+
'default': getattr(p, 'default', None),
|
|
277
|
+
'required': bool(getattr(p, 'required', False)),
|
|
278
|
+
'live': bool(getattr(p, 'live', False)),
|
|
279
|
+
'tags': list(getattr(p, 'tags', [])),
|
|
280
|
+
}
|
|
281
|
+
for p in self.parameters
|
|
282
|
+
]
|
|
283
|
+
return out
|
|
284
|
+
|
|
285
|
+
def bind(self, target: Any) -> BoundCommand:
|
|
286
|
+
"""Bind this command to a compartment instance."""
|
|
287
|
+
return BoundCommand(self, target)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclasses.dataclass(frozen=True)
|
|
291
|
+
class Alias(Command):
|
|
292
|
+
"""A command alias that forwards execution to a compartment command.
|
|
293
|
+
|
|
294
|
+
Declared in NODE_COMMANDS or a compartment's COMMANDS: exposes an alias
|
|
295
|
+
key (e.g. 'strain') that forwards to a target command (e.g. 'strain.show')
|
|
296
|
+
via the node's compartment registry.
|
|
297
|
+
"""
|
|
298
|
+
target: str = ''
|
|
299
|
+
|
|
300
|
+
def __init__(
|
|
301
|
+
self,
|
|
302
|
+
key: str,
|
|
303
|
+
target: str,
|
|
304
|
+
description: str = '',
|
|
305
|
+
reply: str = '',
|
|
306
|
+
parameters: Sequence[Parameter[Any]] = (),
|
|
307
|
+
name: str = '',
|
|
308
|
+
tags: Sequence[str] = (),
|
|
309
|
+
permission: str = '',
|
|
310
|
+
process: Sequence[str] | str = (),
|
|
311
|
+
) -> None:
|
|
312
|
+
super().__init__(
|
|
313
|
+
key=key,
|
|
314
|
+
name=name or key,
|
|
315
|
+
description=description,
|
|
316
|
+
tags=tags,
|
|
317
|
+
parameters=parameters,
|
|
318
|
+
permission=permission,
|
|
319
|
+
process=process,
|
|
320
|
+
run=None,
|
|
321
|
+
reply=reply,
|
|
322
|
+
)
|
|
323
|
+
object.__setattr__(self, 'target', target)
|
|
324
|
+
|
|
325
|
+
def problems(self, cls: type) -> list[str]:
|
|
326
|
+
out = Problems()
|
|
327
|
+
seen: set[str] = set()
|
|
328
|
+
params: tuple[object, ...] = tuple(self.parameters)
|
|
329
|
+
for p in params:
|
|
330
|
+
if not isinstance(p, Parameter):
|
|
331
|
+
out.append(TypeMismatchProblem('COMMANDS', 'node.config.Parameter', type(p).__name__,
|
|
332
|
+
message='alias %s: parameters are node.config.Parameter, not %s' % (self.key, type(p).__name__)))
|
|
333
|
+
continue
|
|
334
|
+
if p.key in seen:
|
|
335
|
+
out.append(DuplicateProblem('COMMANDS', p.key, message='alias %s declares parameter %r twice' % (self.key, p.key)))
|
|
336
|
+
seen.add(p.key)
|
|
337
|
+
if not self.target or not isinstance(self.target, str):
|
|
338
|
+
out.append(Problem('alias %s has no target: name the command it forwards to' % self.key,
|
|
339
|
+
surface='COMMANDS', code='missing_target', target=self.key))
|
|
340
|
+
return out
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def execute(self, target: Any, args: Optional[Mapping[str, Any]] = None) -> Any:
|
|
344
|
+
resolved = self.validate_args(args)
|
|
345
|
+
subsystems = getattr(target, 'subsystems', None)
|
|
346
|
+
if subsystems is None and hasattr(target, 'node'):
|
|
347
|
+
subsystems = getattr(target.node, 'subsystems', None)
|
|
348
|
+
if subsystems is not None:
|
|
349
|
+
return subsystems.dispatch(self.target, resolved)
|
|
350
|
+
raise RuntimeError('cannot dispatch alias %s to %s: target has no subsystems' % (self.key, self.target))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dataclasses.dataclass(frozen=True)
|
|
354
|
+
class BoundCommand:
|
|
355
|
+
"""A Command bound to a specific compartment instance."""
|
|
356
|
+
command: Command
|
|
357
|
+
target: Any
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def key(self) -> str:
|
|
361
|
+
return self.command.key
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def name(self) -> str:
|
|
365
|
+
return self.command.name
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def description(self) -> str:
|
|
369
|
+
return self.command.description
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def tags(self) -> tuple[str, ...]:
|
|
373
|
+
return self.command.tags
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def parameters(self) -> tuple[Parameter[Any], ...]:
|
|
377
|
+
return self.command.parameters
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def permission(self) -> str:
|
|
381
|
+
return self.command.permission
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def process(self) -> tuple[str, ...]:
|
|
386
|
+
return self.command.process
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def __call__(self, args: Optional[Mapping[str, Any]] = None, **kw: Any) -> Any:
|
|
390
|
+
merged: dict[str, ParameterValue] = dict(args or {})
|
|
391
|
+
merged.update(kw)
|
|
392
|
+
return self.command.execute(self.target, merged)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _takes(fn: Callable[..., Any], bound: bool) -> Optional[bool]:
|
|
397
|
+
"""Whether a run method takes the args: True, False, or None when it
|
|
398
|
+
takes something else. `bound` says whether `self` is already applied.
|
|
399
|
+
|
|
400
|
+
The args go only to a REQUIRED positional parameter. A method whose
|
|
401
|
+
positionals all have defaults - `sweep(self, now=None)` - is called bare:
|
|
402
|
+
its optional parameter is its own, and handing it the args dict would
|
|
403
|
+
set `now = {}`."""
|
|
404
|
+
try:
|
|
405
|
+
params = list(inspect.signature(fn).parameters.values())
|
|
406
|
+
except (TypeError, ValueError):
|
|
407
|
+
return None
|
|
408
|
+
if not bound:
|
|
409
|
+
params = params[1:] # self
|
|
410
|
+
positional = [p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
|
|
411
|
+
required = [p for p in positional if p.default is p.empty]
|
|
412
|
+
if len(required) > 1 or any(p.kind == p.KEYWORD_ONLY and p.default is p.empty for p in params):
|
|
413
|
+
return None
|
|
414
|
+
return len(required) == 1
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _coerced(p: Parameter[Any], v: Any) -> Any:
|
|
418
|
+
"""What the caller sent, as the parameter's type. A command line sends
|
|
419
|
+
every value as a string, so an integer or a number given as one is
|
|
420
|
+
taken - and only those: a string is never read as a bool or a list."""
|
|
421
|
+
if p.coerce is not None:
|
|
422
|
+
return p.coerce(v)
|
|
423
|
+
if isinstance(v, bool):
|
|
424
|
+
return v
|
|
425
|
+
if p.type is int and isinstance(v, (str, float)):
|
|
426
|
+
if isinstance(v, float) and not v.is_integer():
|
|
427
|
+
raise ValueError('not a whole number')
|
|
428
|
+
return int(v)
|
|
429
|
+
if p.type is float and isinstance(v, (str, int)):
|
|
430
|
+
return float(v)
|
|
431
|
+
if p.type is tuple and isinstance(v, list):
|
|
432
|
+
return tuple(v)
|
|
433
|
+
return v
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _must(p: Parameter[Any], e: Optional[BaseException] = None) -> str:
|
|
437
|
+
if p.must:
|
|
438
|
+
return 'must ' + p.must
|
|
439
|
+
what = getattr(p.type, '__name__', str(p.type))
|
|
440
|
+
return 'must be %s%s' % ('one of %s' % ', '.join(map(str, p.choices)) if p.choices else 'a ' + what,
|
|
441
|
+
' (%s)' % e if e is not None else '')
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def declared(obj: Any) -> tuple[Command, ...]:
|
|
445
|
+
"""The commands a compartment declares, or () for one not yet converted."""
|
|
446
|
+
return tuple(getattr(obj, 'COMMANDS', ()) or ())
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def every_command(compartments: Sequence[Any]) -> list[CommandRecord]:
|
|
450
|
+
"""Every declared command across compartments."""
|
|
451
|
+
out: list[CommandRecord] = []
|
|
452
|
+
for c in compartments:
|
|
453
|
+
c_name = getattr(c, 'name', '') or getattr(c, 'key', '') or getattr(c, '__name__', str(c))
|
|
454
|
+
for cmd in declared(c):
|
|
455
|
+
row = cmd.describe()
|
|
456
|
+
row['compartment'] = c_name
|
|
457
|
+
out.append(row)
|
|
458
|
+
return out
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""The shared surface a compartment is built from.
|
|
2
|
+
|
|
3
|
+
Every compartment used to be constructed by hand in node/app/core.py from
|
|
4
|
+
whichever of the node's stores and services it happened to need, in one of
|
|
5
|
+
six different signature styles, with `node=self` passed alongside so the
|
|
6
|
+
compartment could reach anything the signature forgot. `NodeContext` is the
|
|
7
|
+
one typed object that replaces that: the stores, the transport, the face, the
|
|
8
|
+
clock, the config, and the handful of node-level callables a compartment
|
|
9
|
+
still has to reach. A compartment's `from_context(ctx)` says, in one place,
|
|
10
|
+
what it takes from the node, and a third party writing a compartment against
|
|
11
|
+
`subsystem/1` has a stable thing to write against.
|
|
12
|
+
|
|
13
|
+
`node` stays on the context as a labelled escape hatch, not a hidden one:
|
|
14
|
+
`scheduler` calls `node.propose`/`node.pull` and `trust` calls
|
|
15
|
+
`node.regrant`, which are NodeApp methods and not stores. Anything reaching
|
|
16
|
+
for `ctx.node` beyond those is a dependency that should be named here instead.
|
|
17
|
+
|
|
18
|
+
Standard library only.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import threading
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Callable, Mapping, NamedTuple, Optional, Sequence, TypeVar
|
|
26
|
+
|
|
27
|
+
from ..channels import NodeEventBus
|
|
28
|
+
from ..interfaces import NostrEvent
|
|
29
|
+
from ..config.base import Block
|
|
30
|
+
from ..config.node import NodeConfigDict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NodeStores(NamedTuple):
|
|
34
|
+
"""The four durable stores a node keeps, as one argument."""
|
|
35
|
+
events: Any # eventstore.NodeStore
|
|
36
|
+
grants: Any # grants.GrantStore
|
|
37
|
+
accounts: Any # accounts.AccountStore
|
|
38
|
+
blobs: Any # blobstore.BlobStore
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _no_descriptor() -> Optional[NostrEvent]:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
B = TypeVar('B', bound=Block)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class NodeContext:
|
|
50
|
+
"""Explicit, typed context handed to every compartment at assembly.
|
|
51
|
+
|
|
52
|
+
`cfg` is the live config dict the node mutates on reload; a compartment
|
|
53
|
+
that must see changes reads through `cfg_of(key)` on each tick rather than
|
|
54
|
+
copying a section at construction. `descriptor` is a getter because the
|
|
55
|
+
descriptor is mined or loaded in start() and does not exist at the time
|
|
56
|
+
the context is made.
|
|
57
|
+
"""
|
|
58
|
+
dir: str
|
|
59
|
+
ks: Any # keystore.Keystore
|
|
60
|
+
cfg: NodeConfigDict
|
|
61
|
+
clock: Callable[[], int]
|
|
62
|
+
events: NodeEventBus
|
|
63
|
+
stores: NodeStores
|
|
64
|
+
relays: list[str]
|
|
65
|
+
transport: Optional[Any] = None # transport.TransportRouter
|
|
66
|
+
router: Optional[Any] = None # router.ProtocolRouter
|
|
67
|
+
face: Optional[Any] = None # relayserver.RelayServer
|
|
68
|
+
host: Optional[Any] = None # transport.host.ProtocolHostRouter
|
|
69
|
+
peers: Optional[Any] = None # peers.Peers
|
|
70
|
+
policy: Optional[Any] = None # fleetmesh_core.peering.Policy
|
|
71
|
+
ledger: Optional[Any] = None # fleetmesh_core.accounting.standing.Ledger
|
|
72
|
+
timeout: float = 15.0
|
|
73
|
+
stop: threading.Event = field(default_factory=threading.Event)
|
|
74
|
+
descriptor: Callable[[], Optional[NostrEvent]] = _no_descriptor
|
|
75
|
+
digest: Optional[str] = None # schema digest this node speaks
|
|
76
|
+
node: Optional[Any] = None # the NodeApp; see module docstring
|
|
77
|
+
says: Optional[Any] = None # node log / channels router
|
|
78
|
+
# Write `cfg` back to the node directory. A compartment that changes
|
|
79
|
+
# what this node claims (coverage, a price) calls this rather than
|
|
80
|
+
# importing the app's config writer - the node owns its directory and
|
|
81
|
+
# the file is what the next tick reads. No-op outside a running node.
|
|
82
|
+
persist_cfg: Callable[[], None] = lambda: None
|
|
83
|
+
# Rebuild and republish the descriptor from what the compartments say
|
|
84
|
+
# now. For a compartment whose declaration can change after start (a
|
|
85
|
+
# sidecar whose self-tests begin or stop passing). No-op outside a
|
|
86
|
+
# running node, where there is no descriptor to correct.
|
|
87
|
+
redescribe: Callable[[], Any] = lambda: None
|
|
88
|
+
# Node-internal methods reached by compartments:
|
|
89
|
+
regrant: Callable[..., Any] = lambda *a, **kw: None
|
|
90
|
+
propose: Callable[..., Any] = lambda *a, **kw: ('declined', None)
|
|
91
|
+
pull: Callable[..., Any] = lambda *a, **kw: {}
|
|
92
|
+
amend: Callable[..., Any] = lambda *a, **kw: (None, None)
|
|
93
|
+
build_descriptor: Callable[[], Any] = lambda: None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def _amend(self) -> Callable[..., Any]:
|
|
97
|
+
return self.amend
|
|
98
|
+
|
|
99
|
+
# ---- derived -------------------------------------------------------------
|
|
100
|
+
@property
|
|
101
|
+
def pubkey(self) -> str:
|
|
102
|
+
return getattr(self.ks, 'node_pk', '')
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def did(self) -> str:
|
|
106
|
+
return getattr(self.ks, 'did', '')
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def node_class(self) -> str:
|
|
110
|
+
return str(self.cfg.get('class', 'edge'))
|
|
111
|
+
|
|
112
|
+
def cfg_of(self, key: str, default: Any = None) -> Any:
|
|
113
|
+
"""A top-level config key as it is *now*, for compartments that read
|
|
114
|
+
live. The same contract `Compartment.config()` keeps."""
|
|
115
|
+
return self.cfg.get(key, default)
|
|
116
|
+
|
|
117
|
+
def cfg_getter(self) -> Callable[[], Mapping[str, Any]]:
|
|
118
|
+
"""The `lambda: self.cfg` several compartments take, named."""
|
|
119
|
+
return lambda: self.cfg
|
|
120
|
+
|
|
121
|
+
def block(self, block_cls: type[B]) -> B:
|
|
122
|
+
"""A compartment's config section as its typed block, *now*.
|
|
123
|
+
|
|
124
|
+
`block_cls.key` names the section; an absent section is the default
|
|
125
|
+
block. The file was checked at the door (load_config) and on every
|
|
126
|
+
reload, so from_dict here cannot meet a shape it refuses - a block
|
|
127
|
+
that reads live calls this each tick and gets the current values.
|
|
128
|
+
"""
|
|
129
|
+
raw = self.cfg.get(block_cls.key)
|
|
130
|
+
return block_cls.from_dict({} if raw is None else raw)
|
|
131
|
+
|
|
132
|
+
def block_getter(self, block_cls: type[B]) -> Callable[[], B]:
|
|
133
|
+
"""`lambda: ctx.block(cls)`, for a compartment that reads live."""
|
|
134
|
+
return lambda: self.block(block_cls)
|
|
135
|
+
|
|
136
|
+
def path(self, *parts: str) -> str:
|
|
137
|
+
return os.path.join(self.dir, *parts)
|
|
138
|
+
|
|
139
|
+
def optional(self, name: str) -> Optional[Any]:
|
|
140
|
+
"""A compartment named in the caller's `uses`, if the node built it;
|
|
141
|
+
None when it is off. Resolved once at construction: the manifest
|
|
142
|
+
orders `uses` before the user, so a present one is already built.
|
|
143
|
+
Tests that assemble without a node fall back to the attribute set on
|
|
144
|
+
`node` by assemble(), which is the same object either way."""
|
|
145
|
+
reg = getattr(self, '_registry', None)
|
|
146
|
+
if reg is not None:
|
|
147
|
+
res = reg.get(name)
|
|
148
|
+
if res is not None:
|
|
149
|
+
return res
|
|
150
|
+
registry = getattr(self.node, 'subsystems', None) if self.node is not None else None
|
|
151
|
+
if registry is not None:
|
|
152
|
+
res = registry.get(name)
|
|
153
|
+
if res is not None:
|
|
154
|
+
return res
|
|
155
|
+
return getattr(self.node, name, None) if self.node is not None else None
|
|
156
|
+
|
|
157
|
+
# ---- the previous keyword-style constructor, kept for callers ------------
|
|
158
|
+
@classmethod
|
|
159
|
+
def build(cls, directory: str, ks: Any, cfg: NodeConfigDict, clock: Callable[[], int],
|
|
160
|
+
events: NodeEventBus, stores: NodeStores, relays: Sequence[str],
|
|
161
|
+
transport: Optional[Any] = None, router: Optional[Any] = None, **more: Any) -> NodeContext:
|
|
162
|
+
return cls(dir=directory, ks=ks, cfg=cfg, clock=clock, events=events, stores=stores,
|
|
163
|
+
relays=list(relays), transport=transport, router=router, **more)
|