Lowers the barrier for sister sites to build their own plugins. Generated output satisfies the framework contract out of the box. CLI command (shopdb/plugins/cli.py): - `flask plugin new <name> --description "..."` generates a plugin skeleton under plugins/<name>/. Validates the name against CONTRIBUTING.md rules (lowercase letters/digits only, no underscores or hyphens, not in the reserved list) and refuses to overwrite existing plugins unless --overwrite is passed. - Output prints the next steps (install, migrate, test). Scaffolder (shopdb/plugins/scaffolder.py): - validate_name: enforces the naming rules - pascal_case: lowercase-to-PascalCase for class names - scaffold_plugin: copies templates with string.Template substitution. Three placeholders: $name, $Name, $description. Files with `model.py` in the path get renamed to <name>.py. Templates (shopdb/plugins/templates/): - manifest.json.tmpl: name, version 0.1.0, description, core_version range >=0.1.0,<1.0.0 (broad enough to survive minor framework bumps) - plugin.py.tmpl: <Name>Plugin class extending BasePlugin with all required hooks implemented (meta from manifest, get_blueprint returning the bp, get_models returning the example model). Includes on_install hook that seeds the AssetType row. - models/__init__.py.tmpl + models/model.py.tmpl: Asset extension table keyed by assetid with one example field. TODO comment marks it as a placeholder. - api/__init__.py.tmpl + api/routes.py.tmpl: Blueprint with list and detail endpoints using the framework's pagination + response helpers. - schemas/__init__.py.tmpl: marshmallow schema stub. - tests/__init__.py.tmpl + tests/test_plugin.py.tmpl: smoke tests asserting plugin loads, get_blueprint returns Blueprint, get_models returns at least one model. - README.md.tmpl: one-pager for plugin authors with common edits and next-step references. Canary tests (tests/test_plugin_scaffold.py): - 14 tests asserting the scaffold output passes contract checks. - Validates name rules (lowercase, reserved, hyphens, digits, etc.) - Verifies all expected files generated, manifest fields present. - Loads the generated plugin via PluginLoader (spec_from_file_location bypasses the real `plugins` package shadowing). - Asserts subclasses BasePlugin, get_blueprint returns Blueprint, get_models returns model with __tablename__. - Module-scoped fixture; cleans up sys.modules + SQLAlchemy metadata on teardown to avoid cross-test contamination. Quickstart docs (docs/PLUGIN-QUICKSTART.md): - 30-minute walkthrough: scaffold -> edit model -> add routes -> install -> verify -> add hooks. Cross-links to PLUGIN-HOOKS.md and the ADRs. Includes common-errors table. Test count: 87 -> 101 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.4 KiB
Plugin Quickstart
Build a working shopdb-flask plugin in 30 minutes. This walks through generating, customizing, installing, and testing a plugin from scratch.
For the full hook reference, see PLUGIN-HOOKS.md. For the architectural decisions behind the contract, see migrations/adr/.
Step 1: Generate the skeleton
flask plugin new cameras --description "Tracks shop-floor surveillance cameras"
Output: plugins/cameras/ with manifest, plugin class, example model, example routes, schemas stub, tests, and a README.
The generated plugin already passes the framework's contract tests. Verify before editing:
pytest plugins/cameras/tests/
Step 2: Edit the model
Open plugins/cameras/models/cameras.py. Replace the examplefield placeholder with your domain fields:
class Cameras(BaseModel):
__tablename__ = 'cameras'
assetid = db.Column(
db.Integer,
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
primary_key=True,
)
streamurl = db.Column(db.String(255), nullable=False)
resolution = db.Column(db.String(20))
fps = db.Column(db.Integer)
poeport = db.Column(db.String(50))
asset = db.relationship('Asset', backref=db.backref('cameras', uselist=False))
def to_dict(self):
return {
'assetid': self.assetid,
'streamurl': self.streamurl,
'resolution': self.resolution,
'fps': self.fps,
'poeport': self.poeport,
}
Note the naming convention: lowercase concatenated, no underscores (streamurl, not stream_url). See CONTRIBUTING.md.
Step 3: Add routes
Open plugins/cameras/api/routes.py. The scaffold provides list and detail endpoints. Add CRUD as needed:
@cameras_bp.route('', methods=['POST'])
@jwt_required()
def create_camera():
data = request.get_json()
asset = Asset(assetnumber=data['assetnumber'], name=data['name'], ...)
db.session.add(asset)
db.session.flush()
camera = Cameras(
assetid=asset.assetid,
streamurl=data['streamurl'],
resolution=data.get('resolution'),
)
db.session.add(camera)
db.session.commit()
return success_response(camera.to_dict(), http_code=201)
For audit logging, use the public helper:
from shopdb.api import audit_log
audit_log(action='created', entitytype='Camera', entityid=asset.assetid, entityname=asset.name)
Step 4: Install the plugin
flask plugin install cameras
flask db migrate -m "Add cameras plugin tables"
flask db upgrade
install runs the plugin's on_install hook (which seeds the AssetType row), registers it in the plugin registry, and runs migrations.
Step 5: Verify it works
flask plugin list
You should see cameras [Enabled].
Run the plugin's tests:
pytest plugins/cameras/tests/
Hit the API:
curl http://localhost:5001/api/cameras
Step 6: Add hooks (optional)
Override hooks on the plugin class as needed. See PLUGIN-HOOKS.md for the full list. Common ones:
| Hook | Adds |
|---|---|
get_searchable_fields |
Plugin contributes to the global search endpoint |
get_navigation_items |
Plugin shows up in the sidebar nav |
get_dashboard_widgets |
Plugin's dashboard widget appears on the home page |
get_collector_schema |
Plugin accepts external pushes at /api/collector/<name> |
Each hook has a default that does nothing. Override only what your plugin needs.
Step 7: Frontend (manual for now)
Backend scaffolding is automated. Frontend is manual until the frontend scaffolding skill ships. Convention:
frontend/src/views/cameras/CamerasList.vuefrontend/src/views/cameras/CameraDetail.vuefrontend/src/views/cameras/CameraForm.vue
Copy from an existing plugin's view files (e.g., frontend/src/views/network/) as a starting point. Update the API client in frontend/src/api/index.js to add cameras endpoints.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
PluginNotFoundError: manifest.json |
Manifest deleted or moved | Restore plugins/<name>/manifest.json |
PluginContractError: missing required field |
manifest.json incomplete | Re-add name, version, description |
PluginVersionError: requires core_version X but framework is Y |
Framework upgraded past your range | Update core_version in manifest |
Table 'cameras' is already defined |
Two models declared the same __tablename__ |
Pick a unique table name |
| Index name collision | Two indexes share the same name (SQLite enforces global uniqueness) | Prefix index names with table: idx_cameras_streamurl |
Next steps
- PLUGIN-HOOKS.md for the full hook reference
- CONTRIBUTING.md for naming conventions
- migrations/adr/ADR-001-asset-as-platform-contract.md for what your plugin can rely on
- migrations/adr/ADR-006-collector-contract.md for accepting external collector input
Distribution
If you are building a plugin for a specific GE Aerospace site (sister-site adoption), ship it as its own git repo. The site running shopdb-flask clones or symlinks your plugin into <repo>/plugins/<name>/. See ADR-003.