Observed since 20 Oct, 20:29 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Book management CLI demonstrating hexagonal architecture
A minimal example demonstrating the ports & adapters (hexagonal) pattern
with a Typer CLI, mirroring the structure used in the smus package.
book_manager/
├── __init__.py ← Factory (wires adapter into service)
├── __main__.py ← Entry point: python -m book_manager
├── model.py ← Data classes (Book, requests)
├── ports.py ← Protocol (interface) — what the service expects
├── service.py ← Business logic — only talks to the port
├── cli.py ← Typer commands — parses args, calls service
└── adapters/
└── json_store.py ← Concrete implementation (stores books as JSON)
tests/
└── test_service.py ← Unit tests with mocked port
CLI (cli.py)
│ parses user input, calls service
▼
Service (service.py)
│ contains business rules, uses self._store (typed as BookStorePort)
▼
Port (ports.py)
│ Protocol class — defines method signatures only
▼
Adapter (adapters/json_store.py)
implements the port methods, talks to the filesystem
# With uv (recommended)
uv sync --extra dev
# Or with pip
pip install -e ".[dev]"
# Add a book
uv run books add --isbn "978-0441172719" --title "Dune" --author "Frank Herbert"
# List all books
uv run books list
# Get a specific book
uv run books get --isbn "978-0441172719"
# Remove a book
uv run books remove --isbn "978-0441172719"
The store location defaults to books.json in the current directory. Override it
with the BOOKS_FILE environment variable:
BOOKS_FILE=data/library.json uv run books list
uv run pytest
The CLI is published on PyPI as aerkoc-bookstore, so consumers install it directly
from there — no token, no repo access needed. Three entry points are available:
| Entry point | File | Use case |
|---|---|---|
| Inline reusable workflow | book-manager-inline.yml | Turnkey job, all steps self-contained |
| Action-backed reusable workflow | book-manager.yml | Turnkey job, delegates to the composite action |
| Composite action | .github/actions/book-manager | Drop-in step inside your own job |
Minimal example in a consumer repo (.github/workflows/manage-books.yml):
name: Manage bookstore
on:
workflow_dispatch:
inputs:
command: { type: choice, options: [add, list, get, remove], required: true }
isbn: { type: string, required: false }
title: { type: string, required: false }
author: { type: string, required: false }
jobs:
books:
uses: sly01/bookstore/.github/workflows/book-manager-inline.yml@v0.1.0
with:
command: ${{ github.event.inputs.command }}
isbn: ${{ github.event.inputs.isbn }}
title: ${{ github.event.inputs.title }}
author: ${{ github.event.inputs.author }}
books-file: books.json
commit-changes: true
# version: "0.1.0" # pin to a specific PyPI release; omit for latest
No secrets: block needed. A full copy-paste example with all three options lives in
examples/consumer-manage-books.yml.
Pushing a version tag triggers the publish workflow automatically:
git tag v0.1.0
git push origin v0.1.0
This builds the wheel and uploads it to pypi.org/project/aerkoc-bookstore/ using Trusted Publisher (OIDC — no API
token stored in secrets). See .github/workflows/publish.yml for the one-time PyPI
setup instructions.
The service never imports the adapter. It only knows about BookStorePort.
This means you can:
JsonBookStore for a PostgresBookStore without changing the serviceMagicMock(spec=BookStorePort) — no file I/O neededDownload counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.0 | 12 Jun 2026 | 2 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 20:29 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:1fdbd51b443bcd157345509f2456c4b77ab5eec8f4fbc726bde57fe1596a153a