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,290 @@
|
|
|
1
|
+
"""SCHEMA: what a compartment persists in a relational database, declared where it is.
|
|
2
|
+
|
|
3
|
+
A compartment declares its database schema as a tuple of typed entries - Table
|
|
4
|
+
and Index - on the class attribute SCHEMA. Like COMMANDS, WORKERS, and CHANNELS,
|
|
5
|
+
it is declared once, validated at class definition time by the base
|
|
6
|
+
Compartment, and instantiated into a database adapter module (sqlite, psql, etc.)
|
|
7
|
+
when the compartment is initialized.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Optional, Sequence, Union
|
|
14
|
+
|
|
15
|
+
from .problem import (
|
|
16
|
+
DuplicateProblem,
|
|
17
|
+
IncompatibleProblem,
|
|
18
|
+
InvalidIdentifierProblem,
|
|
19
|
+
Problem,
|
|
20
|
+
Problems,
|
|
21
|
+
ReferenceProblem,
|
|
22
|
+
TypeMismatchProblem,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(frozen=True)
|
|
27
|
+
class Column:
|
|
28
|
+
"""One typed column of a database table."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
type: str
|
|
32
|
+
nullable: bool = True
|
|
33
|
+
primary_key: bool = False
|
|
34
|
+
default: Any = None
|
|
35
|
+
unique: bool = False
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
type: str,
|
|
41
|
+
*,
|
|
42
|
+
nullable: bool = True,
|
|
43
|
+
not_null: bool = False,
|
|
44
|
+
primary_key: bool = False,
|
|
45
|
+
default: Any = None,
|
|
46
|
+
unique: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
object.__setattr__(self, 'name', str(name))
|
|
49
|
+
object.__setattr__(self, 'type', str(type).upper())
|
|
50
|
+
is_nullable = False if not_null else nullable
|
|
51
|
+
if primary_key:
|
|
52
|
+
is_nullable = False
|
|
53
|
+
object.__setattr__(self, 'nullable', is_nullable)
|
|
54
|
+
object.__setattr__(self, 'primary_key', bool(primary_key))
|
|
55
|
+
object.__setattr__(self, 'default', default)
|
|
56
|
+
object.__setattr__(self, 'unique', bool(unique))
|
|
57
|
+
|
|
58
|
+
def to_sql(self, dialect: str = 'sqlite') -> str:
|
|
59
|
+
"""Render column specification in DDL."""
|
|
60
|
+
parts = [self.name, self.type]
|
|
61
|
+
if self.primary_key:
|
|
62
|
+
parts.append('PRIMARY KEY')
|
|
63
|
+
elif not self.nullable:
|
|
64
|
+
parts.append('NOT NULL')
|
|
65
|
+
if self.default is not None:
|
|
66
|
+
parts.append(f'DEFAULT {self.default}')
|
|
67
|
+
if self.unique:
|
|
68
|
+
parts.append('UNIQUE')
|
|
69
|
+
return ' '.join(parts)
|
|
70
|
+
|
|
71
|
+
def problems(self, table_name: str = '') -> list[str]:
|
|
72
|
+
out = Problems()
|
|
73
|
+
loc = f' in table {table_name}' if table_name else ''
|
|
74
|
+
if not self.name or not isinstance(self.name, str) or not re.match(r'^[A-Za-z_]\w*$', self.name):
|
|
75
|
+
out.append(InvalidIdentifierProblem('SCHEMA', self.name, f'column name {self.name!r}{loc} is not a valid identifier'))
|
|
76
|
+
if not self.type or not isinstance(self.type, str):
|
|
77
|
+
out.append(Problem(f'column {self.name}{loc} must declare a SQL type', surface='SCHEMA', code='missing_type', target=self.name))
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclasses.dataclass(frozen=True)
|
|
82
|
+
class Index:
|
|
83
|
+
"""An index over one or more columns in a table."""
|
|
84
|
+
|
|
85
|
+
name: str
|
|
86
|
+
columns: tuple[str, ...]
|
|
87
|
+
table: str = ''
|
|
88
|
+
unique: bool = False
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
name: str,
|
|
93
|
+
columns: Sequence[str] | str,
|
|
94
|
+
*,
|
|
95
|
+
table: str = '',
|
|
96
|
+
unique: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
object.__setattr__(self, 'name', str(name))
|
|
99
|
+
cols = (columns,) if isinstance(columns, str) else tuple(str(c) for c in columns)
|
|
100
|
+
object.__setattr__(self, 'columns', cols)
|
|
101
|
+
object.__setattr__(self, 'table', str(table))
|
|
102
|
+
object.__setattr__(self, 'unique', bool(unique))
|
|
103
|
+
|
|
104
|
+
def bind_table(self, table: str) -> Index:
|
|
105
|
+
"""Return a copy of this index bound to a table name if not set."""
|
|
106
|
+
if self.table:
|
|
107
|
+
return self
|
|
108
|
+
return Index(self.name, self.columns, table=table, unique=self.unique)
|
|
109
|
+
|
|
110
|
+
def to_sql(self, dialect: str = 'sqlite') -> str:
|
|
111
|
+
"""Render CREATE INDEX statement."""
|
|
112
|
+
unique_clause = 'UNIQUE ' if self.unique else ''
|
|
113
|
+
cols = ', '.join(self.columns)
|
|
114
|
+
tbl = self.table or 'unknown_table'
|
|
115
|
+
return f'CREATE {unique_clause}INDEX IF NOT EXISTS {self.name} ON {tbl}({cols})'
|
|
116
|
+
|
|
117
|
+
def problems(self, table: Optional[Table] = None) -> list[str]:
|
|
118
|
+
out = Problems()
|
|
119
|
+
if not self.name or not isinstance(self.name, str) or not re.match(r'^[A-Za-z_]\w*$', self.name):
|
|
120
|
+
out.append(InvalidIdentifierProblem('SCHEMA', self.name, f'index name {self.name!r} is not a valid identifier'))
|
|
121
|
+
tbl = self.table or (table.name if table else '')
|
|
122
|
+
if not tbl:
|
|
123
|
+
out.append(Problem(f'index {self.name} must specify a table', surface='SCHEMA', code='missing_table', target=self.name))
|
|
124
|
+
if not self.columns:
|
|
125
|
+
out.append(Problem(f'index {self.name} must index at least one column', surface='SCHEMA', code='empty_index', target=self.name))
|
|
126
|
+
if table is not None:
|
|
127
|
+
valid_cols = {c.name for c in table.columns if isinstance(c, Column)}
|
|
128
|
+
for c in self.columns:
|
|
129
|
+
if c not in valid_cols:
|
|
130
|
+
out.append(ReferenceProblem('SCHEMA', f'index {self.name}', c, f'index {self.name} indexes unknown column {c!r} on table {table.name}'))
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclasses.dataclass(frozen=True)
|
|
135
|
+
class Table:
|
|
136
|
+
"""A database table with typed columns, primary key, and optional indices."""
|
|
137
|
+
|
|
138
|
+
name: str
|
|
139
|
+
columns: tuple[Column, ...]
|
|
140
|
+
primary_key: tuple[str, ...] = ()
|
|
141
|
+
indices: tuple[Index, ...] = ()
|
|
142
|
+
without_rowid: bool = False
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
name: str,
|
|
147
|
+
columns: Sequence[Column],
|
|
148
|
+
*,
|
|
149
|
+
primary_key: Sequence[str] | str = (),
|
|
150
|
+
indices: Sequence[Index] = (),
|
|
151
|
+
without_rowid: bool = False,
|
|
152
|
+
) -> None:
|
|
153
|
+
object.__setattr__(self, 'name', str(name))
|
|
154
|
+
object.__setattr__(self, 'columns', tuple(columns))
|
|
155
|
+
pk = (primary_key,) if isinstance(primary_key, str) else tuple(str(k) for k in primary_key)
|
|
156
|
+
object.__setattr__(self, 'primary_key', pk)
|
|
157
|
+
bound_indices = tuple(idx.bind_table(name) if isinstance(idx, Index) else idx for idx in indices)
|
|
158
|
+
object.__setattr__(self, 'indices', bound_indices)
|
|
159
|
+
object.__setattr__(self, 'without_rowid', bool(without_rowid))
|
|
160
|
+
|
|
161
|
+
def column_names(self) -> tuple[str, ...]:
|
|
162
|
+
return tuple(c.name for c in self.columns if isinstance(c, Column))
|
|
163
|
+
|
|
164
|
+
def pk_names(self) -> tuple[str, ...]:
|
|
165
|
+
if self.primary_key:
|
|
166
|
+
return self.primary_key
|
|
167
|
+
return tuple(c.name for c in self.columns if isinstance(c, Column) and c.primary_key)
|
|
168
|
+
|
|
169
|
+
def column(self, name: str) -> Optional[Column]:
|
|
170
|
+
for c in self.columns:
|
|
171
|
+
if isinstance(c, Column) and c.name == name:
|
|
172
|
+
return c
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def to_sql(self, dialect: str = 'sqlite') -> str:
|
|
176
|
+
"""Render CREATE TABLE statement."""
|
|
177
|
+
col_defs = [c.to_sql(dialect) for c in self.columns if isinstance(c, Column)]
|
|
178
|
+
inline_pks = [c.name for c in self.columns if isinstance(c, Column) and c.primary_key]
|
|
179
|
+
if self.primary_key and not inline_pks:
|
|
180
|
+
col_defs.append(f"PRIMARY KEY ({', '.join(self.primary_key)})")
|
|
181
|
+
body = ', '.join(col_defs)
|
|
182
|
+
sql = f'CREATE TABLE IF NOT EXISTS {self.name} ({body})'
|
|
183
|
+
if self.without_rowid and dialect == 'sqlite':
|
|
184
|
+
sql += ' WITHOUT ROWID'
|
|
185
|
+
return sql
|
|
186
|
+
|
|
187
|
+
def secondary_sql(self, dialect: str = 'sqlite') -> list[str]:
|
|
188
|
+
"""Render all attached indices as DDL statements."""
|
|
189
|
+
return [idx.to_sql(dialect) for idx in self.indices if isinstance(idx, Index)]
|
|
190
|
+
|
|
191
|
+
def problems(self) -> list[str]:
|
|
192
|
+
out = Problems()
|
|
193
|
+
if not self.name or not isinstance(self.name, str) or not re.match(r'^[A-Za-z_]\w*$', self.name):
|
|
194
|
+
out.append(InvalidIdentifierProblem('SCHEMA', self.name, f'table name {self.name!r} is not a valid identifier'))
|
|
195
|
+
if not self.columns:
|
|
196
|
+
out.append(Problem(f'table {self.name} must declare at least one column', surface='SCHEMA', code='empty_table', target=self.name))
|
|
197
|
+
return out
|
|
198
|
+
seen_cols: set[str] = set()
|
|
199
|
+
for c_any in self.columns:
|
|
200
|
+
c_val: Any = c_any
|
|
201
|
+
if not isinstance(c_val, Column):
|
|
202
|
+
out.append(TypeMismatchProblem('SCHEMA', 'Column', type(c_val).__name__, f'table {self.name} column must be Column, got {type(c_val).__name__}'))
|
|
203
|
+
continue
|
|
204
|
+
if c_val.name in seen_cols:
|
|
205
|
+
out.append(DuplicateProblem('SCHEMA', c_val.name, f'table {self.name} declares column {c_val.name!r} twice'))
|
|
206
|
+
seen_cols.add(c_val.name)
|
|
207
|
+
out.extend(c_val.problems(self.name))
|
|
208
|
+
|
|
209
|
+
inline_pks = [c.name for c in self.columns if isinstance(c, Column) and c.primary_key]
|
|
210
|
+
if inline_pks and self.primary_key:
|
|
211
|
+
out.append(IncompatibleProblem('SCHEMA', self.name, f'table {self.name} declares both inline PRIMARY KEY and table primary_key={self.primary_key}'))
|
|
212
|
+
elif self.primary_key:
|
|
213
|
+
for pk in self.primary_key:
|
|
214
|
+
if pk not in seen_cols:
|
|
215
|
+
out.append(ReferenceProblem('SCHEMA', f'table {self.name}', pk, f'table {self.name} primary key references unknown column {pk!r}'))
|
|
216
|
+
|
|
217
|
+
seen_indices: set[str] = set()
|
|
218
|
+
for idx_any in self.indices:
|
|
219
|
+
idx_val: Any = idx_any
|
|
220
|
+
if not isinstance(idx_val, Index):
|
|
221
|
+
out.append(TypeMismatchProblem('SCHEMA', 'Index', type(idx_val).__name__, f'table {self.name} index must be Index, got {type(idx_val).__name__}'))
|
|
222
|
+
continue
|
|
223
|
+
if idx_val.name in seen_indices:
|
|
224
|
+
out.append(DuplicateProblem('SCHEMA', idx_val.name, f'table {self.name} declares index {idx_val.name!r} twice'))
|
|
225
|
+
seen_indices.add(idx_val.name)
|
|
226
|
+
out.extend(idx_val.problems(self))
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclasses.dataclass(frozen=True)
|
|
231
|
+
class Migration:
|
|
232
|
+
"""A versioned database migration."""
|
|
233
|
+
|
|
234
|
+
version: int
|
|
235
|
+
up: tuple[str, ...]
|
|
236
|
+
down: Optional[tuple[str, ...]] = None
|
|
237
|
+
name: str = ''
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
version: int,
|
|
242
|
+
up: str | Sequence[str],
|
|
243
|
+
*,
|
|
244
|
+
down: Optional[str | Sequence[str]] = None,
|
|
245
|
+
name: str = '',
|
|
246
|
+
) -> None:
|
|
247
|
+
object.__setattr__(self, 'version', int(version))
|
|
248
|
+
up_sql = (up,) if isinstance(up, str) else tuple(str(s) for s in up)
|
|
249
|
+
object.__setattr__(self, 'up', up_sql)
|
|
250
|
+
down_sql = None
|
|
251
|
+
if down is not None:
|
|
252
|
+
down_sql = (down,) if isinstance(down, str) else tuple(str(s) for s in down)
|
|
253
|
+
object.__setattr__(self, 'down', down_sql)
|
|
254
|
+
object.__setattr__(self, 'name', str(name))
|
|
255
|
+
|
|
256
|
+
def to_tuple(self) -> tuple[int, str]:
|
|
257
|
+
"""Convert to (version, sql) pair for BaseStore compatibility."""
|
|
258
|
+
return (self.version, '; '.join(self.up))
|
|
259
|
+
|
|
260
|
+
def problems(self) -> list[str]:
|
|
261
|
+
out = Problems()
|
|
262
|
+
if self.version <= 0:
|
|
263
|
+
out.append(Problem(
|
|
264
|
+
f'migration version must be a positive integer, got {self.version}',
|
|
265
|
+
surface='MIGRATIONS',
|
|
266
|
+
code='invalid_version',
|
|
267
|
+
target=str(self.version),
|
|
268
|
+
))
|
|
269
|
+
if not self.up:
|
|
270
|
+
out.append(Problem(
|
|
271
|
+
f'migration {self.version} must declare up SQL statements',
|
|
272
|
+
surface='MIGRATIONS',
|
|
273
|
+
code='empty_migration',
|
|
274
|
+
target=str(self.version),
|
|
275
|
+
))
|
|
276
|
+
for stmt in self.up:
|
|
277
|
+
if not stmt or not stmt.strip():
|
|
278
|
+
out.append(Problem(
|
|
279
|
+
f'migration {self.version} has empty SQL statement',
|
|
280
|
+
surface='MIGRATIONS',
|
|
281
|
+
code='empty_statement',
|
|
282
|
+
target=str(self.version),
|
|
283
|
+
))
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
SchemaEntry = Union[Table, Index]
|
|
288
|
+
|
|
289
|
+
__all__ = ['Column', 'Index', 'Table', 'Migration', 'SchemaEntry']
|
|
290
|
+
|