Problem
The models that describe the Node API's responses live in the modules that serve them. Importing one therefore costs the whole serving stack — FastAPI, every router, and the pqn_hardware driver imports they pull in — even when the caller only wants a two-field schema.
Measured on this branch:
| Module |
Holds |
Import cost |
pqn_node/core/config.py |
GamesAvailability |
62ms, and builds the Settings singleton as a side effect (reads whatever config.toml is in the working directory) |
pqn_node/api/main.py |
NodeConfig |
176ms — includes all eight routers |
pqn_node/api/routes/health.py |
HealthStatus, ComponentStatus, DeviceStatus |
154ms |
pqn_node/api/routes/chsh.py |
ChshResult |
157ms |
There is no module a consumer can import a response schema from for free.
Two consequences are already in the tree:
-
daily_report.py:22-24 imports three models out of route modules — ChshResult from api/routes/chsh.py, ComponentStatus and HealthStatus from api/routes/health.py. It works because daily_report runs on a Node, where the serving stack is loaded anyway.
-
pqn_whobot has started duplicating instead. pqn_whobot/node_client.py:NodeConfigResponse restates api/main.py:NodeConfig rather than importing it, and the two are already drifting (follower_node_address is a required nullable key on one and defaulted on the other). Whobot may not run on a Node at all, so paying 176ms and loading hardware drivers to get two fields is not a reasonable trade.
This gets worse, not better: the Whobot Daily Digest (WHOBOT.md Phase 5) is a port of daily_report.py, so it needs exactly HealthStatus and ChshResult — the two in route modules. Importing a FastAPI router into a Slack bot is the point at which duplication starts to look like the cheaper option, which is how schemas silently fork.
Proposal
Add a module — pqn_node/api/models.py or similar — that contains only pydantic models and imports nothing but pydantic. Move the shared schemas there and re-export them from where they live today, so no existing import breaks and the OpenAPI schema is unchanged.
Known models to move:
| Model |
Currently in |
Wanted by |
NodeConfig |
api/main.py:18 |
pqn_whobot — Node Info action, registry name resolution |
HealthStatus |
api/routes/health.py:52 |
daily_report.py, Whobot Phase 5 digest |
ComponentStatus |
api/routes/health.py:40 |
as above (HealthStatus is built from it) |
DeviceStatus |
api/routes/health.py:46 |
as above |
ChshResult |
api/routes/chsh.py:26 |
daily_report.py, Whobot Phase 5 digest |
GamesAvailability |
core/config.py:59 |
pqn_whobot — Change Game availability action |
GamesAvailability is the debatable one. It is genuinely a configuration model as well as a response model, so moving it out of core/config.py may be wrong. Worth deciding explicitly rather than by default — the reason it is on the list is that importing it today drags in the Settings singleton build.
Not solved by this
Sharing the module does not make NodeConfig usable by Whobot as-is. The server guarantees node_name (Settings.node_name: str, core/config.py:66) so its response model requires it; a client must treat it as optional, because a Node running older code answers without one and is out of date rather than unreachable — a distinction pqn_whobot deliberately reports (see registry.py). Either the shared model relaxes that field, or the client keeps a lenient variant and says why. There is a FIXME on pqn_whobot/node_client.py:NodeConfigResponse tracking that half.
Done when
- A models module exists that imports nothing but pydantic, verified by an import-cost check or a test.
- The models above live there and are re-exported from their current homes; the OpenAPI schema is unchanged.
daily_report.py imports from it rather than from route modules.
- A decision is recorded on
GamesAvailability and on NodeConfig's optional-vs-required node_name.
Problem
The models that describe the Node API's responses live in the modules that serve them. Importing one therefore costs the whole serving stack — FastAPI, every router, and the
pqn_hardwaredriver imports they pull in — even when the caller only wants a two-field schema.Measured on this branch:
pqn_node/core/config.pyGamesAvailabilitySettingssingleton as a side effect (reads whateverconfig.tomlis in the working directory)pqn_node/api/main.pyNodeConfigpqn_node/api/routes/health.pyHealthStatus,ComponentStatus,DeviceStatuspqn_node/api/routes/chsh.pyChshResultThere is no module a consumer can import a response schema from for free.
Two consequences are already in the tree:
daily_report.py:22-24imports three models out of route modules —ChshResultfromapi/routes/chsh.py,ComponentStatusandHealthStatusfromapi/routes/health.py. It works becausedaily_reportruns on a Node, where the serving stack is loaded anyway.pqn_whobothas started duplicating instead.pqn_whobot/node_client.py:NodeConfigResponserestatesapi/main.py:NodeConfigrather than importing it, and the two are already drifting (follower_node_addressis a required nullable key on one and defaulted on the other). Whobot may not run on a Node at all, so paying 176ms and loading hardware drivers to get two fields is not a reasonable trade.This gets worse, not better: the Whobot Daily Digest (WHOBOT.md Phase 5) is a port of
daily_report.py, so it needs exactlyHealthStatusandChshResult— the two in route modules. Importing a FastAPI router into a Slack bot is the point at which duplication starts to look like the cheaper option, which is how schemas silently fork.Proposal
Add a module —
pqn_node/api/models.pyor similar — that contains only pydantic models and imports nothing but pydantic. Move the shared schemas there and re-export them from where they live today, so no existing import breaks and the OpenAPI schema is unchanged.Known models to move:
NodeConfigapi/main.py:18pqn_whobot— Node Info action, registry name resolutionHealthStatusapi/routes/health.py:52daily_report.py, Whobot Phase 5 digestComponentStatusapi/routes/health.py:40HealthStatusis built from it)DeviceStatusapi/routes/health.py:46ChshResultapi/routes/chsh.py:26daily_report.py, Whobot Phase 5 digestGamesAvailabilitycore/config.py:59pqn_whobot— Change Game availability actionGamesAvailabilityis the debatable one. It is genuinely a configuration model as well as a response model, so moving it out ofcore/config.pymay be wrong. Worth deciding explicitly rather than by default — the reason it is on the list is that importing it today drags in theSettingssingleton build.Not solved by this
Sharing the module does not make
NodeConfigusable by Whobot as-is. The server guaranteesnode_name(Settings.node_name: str,core/config.py:66) so its response model requires it; a client must treat it as optional, because a Node running older code answers without one and is out of date rather than unreachable — a distinctionpqn_whobotdeliberately reports (seeregistry.py). Either the shared model relaxes that field, or the client keeps a lenient variant and says why. There is aFIXMEonpqn_whobot/node_client.py:NodeConfigResponsetracking that half.Done when
daily_report.pyimports from it rather than from route modules.GamesAvailabilityand onNodeConfig's optional-vs-requirednode_name.