Files
pocketpaw/ee/fleet/models.py
Prakash Dalai df5c77dee2 feat(fleet): installable bundle runtime + Sales Fleet template (#940)
* feat(fleet): installable bundle runtime + Sales Fleet template

A FleetTemplate is a YAML manifest naming a soul template + pocket +
connector list + scope tags. install_fleet() orchestrates the install
using existing primitives — SoulFactory, ConnectorRegistry, pocket
service — without introducing new runtime concepts. Sales Fleet ships
as the first bundled example.

What landed:

- ee/fleet/models.py
  - FleetConnector — name + config + optional flag.
  - FleetTemplate — name, display_name, version, soul_template ref,
    pocket name + widgets, connector list, scope list, open metadata.
  - FleetInstallStep + FleetInstallReport — per-step status
    (succeeded/skipped/failed) so partial installs are observable
    without re-running the whole pipeline.

- ee/fleet/installer.py
  - load_fleet(path_or_name) reads YAML/JSON or resolves a bundled
    name (sales-fleet → src/pocketpaw/fleet_templates/sales-fleet.yaml).
  - install_fleet(fleet, *, soul_factory, connector_registry,
    pocket_creator) — pure orchestrator. Each external dep is
    injectable so tests substitute fakes; production callers pass
    the real services.
  - Each step is wrapped: per-step exceptions are caught + logged
    + marked as failed in the report so install never crashes the
    runtime.
  - Optional connectors get "skipped" when missing; required get
    "failed" so admins see what to fix.

- src/pocketpaw/fleet_templates/sales-fleet.yaml
  - Arrow soul + Pipeline pocket + HubSpot + Gong connectors,
    scoped org:sales:*. Connectors marked optional so the demo
    install works without external API keys.

Tests: 15 new in tests/cloud/test_fleet_installer.py covering:
  - YAML + JSON manifest loading + bundled-by-name resolution +
    missing-file error
  - install_fleet creates soul + pocket + registers connectors
    with mocked deps
  - Skips pocket cleanly when creator unavailable
  - Optional missing connector → skipped, required missing → failed
  - Per-step exception is captured in the report
  - Returns early on soul creation failure (no orphan pocket)
  - Sales Fleet bundled, has Arrow soul + sales scope
  - Sales Fleet connectors all optional (demo-friendly)
  - Report.succeeded() + failed_steps() helpers

First PR of Move 7 PR-B. PR-C ships the Install Fleet UI.

* style(fleet): ruff auto-fix

* fix(fleet): point PyYAML import error at pocketpaw[soul]

PyYAML is pulled in transitively via pocketpaw[soul] -> soul-protocol[engine].
The error message was pointing at the transitive package, which sent operators
chasing the wrong install command. Point it at the pocketpaw extra that
actually owns the dependency.

---------

Co-authored-by: Prakash-1 <prakash-1@Mac.lan>
2026-04-16 09:30:34 +05:30

65 lines
2.2 KiB
Python

# ee/fleet/models.py — FleetTemplate manifest + install report types.
# Created: 2026-04-13 (Move 7 PR-B) — A fleet is a thin orchestration over
# primitives that already exist (soul template, pocket, connectors, scope).
# No new runtime concepts; the manifest just names them in one place so a
# non-technical operator can install the whole bundle in one step.
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, Field
class FleetConnector(BaseModel):
"""One connector to register when the fleet is installed."""
name: str
config: dict[str, Any] = Field(default_factory=dict)
optional: bool = False # Skip silently if the connector module is missing.
class FleetTemplate(BaseModel):
"""An installable bundle of soul + pocket + connectors + scopes."""
name: str
display_name: str = ""
description: str = ""
version: str = "0.1.0"
soul_template: str # Bundled soul template name (arrow / flash / cyborg / analyst)
soul_name: str = "" # Override; defaults to template's name
pocket_name: str # Pocket created at install time
pocket_description: str = ""
pocket_widgets: list[dict[str, Any]] = Field(default_factory=list)
connectors: list[FleetConnector] = Field(default_factory=list)
scopes: list[str] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
class FleetInstallStep(BaseModel):
"""One step in the install pipeline. Reports succeeded / skipped / failed
so the UI can show partial progress without re-running the whole install.
"""
name: str
status: Literal["succeeded", "skipped", "failed"]
detail: str = ""
duration_ms: int = 0
class FleetInstallReport(BaseModel):
"""Full report of an install run."""
fleet: str
installed_at: datetime = Field(default_factory=datetime.now)
steps: list[FleetInstallStep] = Field(default_factory=list)
soul_id: str | None = None
pocket_id: str | None = None
def succeeded(self) -> bool:
return all(step.status != "failed" for step in self.steps)
def failed_steps(self) -> list[FleetInstallStep]:
return [s for s in self.steps if s.status == "failed"]