Book management CLI demonstrating hexagonal architecture
Project description
Book Manager — Hexagonal Architecture Example
A minimal example demonstrating the ports & adapters (hexagonal) pattern
with a Typer CLI, mirroring the structure used in the smus package.
Structure
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
How the layers connect
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
Install
# With uv (recommended)
uv sync --extra dev
# Or with pip
pip install -e ".[dev]"
Run it
# 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
Run tests
uv run pytest
Use it from another repo's GitHub Actions
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.
Publishing a release
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.
Key takeaway
The service never imports the adapter. It only knows about BookStorePort.
This means you can:
- Swap
JsonBookStorefor aPostgresBookStorewithout changing the service - In tests, inject a
MagicMock(spec=BookStorePort)— no file I/O needed - The CLI is just a thin shell that parses args and delegates to the service
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aerkoc_bookstore-0.1.0.tar.gz.
File metadata
- Download URL: aerkoc_bookstore-0.1.0.tar.gz
- Upload date:
- Size: 17.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3daeaf22bb05f826ec21c6a7b33594f7ed1d51c289c454350842358031430ae
|
|
| MD5 |
a0e3da22744a0951a40e32dfee99806c
|
|
| BLAKE2b-256 |
c6d08116c75912bc98b82dcb8b1f2a614adebce03309fb607a8630ea41ed72f9
|
Provenance
The following attestation bundles were made for aerkoc_bookstore-0.1.0.tar.gz:
Publisher:
publish.yml on sly01/bookstore
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aerkoc_bookstore-0.1.0.tar.gz -
Subject digest:
d3daeaf22bb05f826ec21c6a7b33594f7ed1d51c289c454350842358031430ae - Sigstore transparency entry: 1805069331
- Sigstore integration time:
-
Permalink:
sly01/bookstore@81d274e053357c1d9eb6e6b8d019822bf1761e68 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sly01
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81d274e053357c1d9eb6e6b8d019822bf1761e68 -
Trigger Event:
push
-
Statement type:
File details
Details for the file aerkoc_bookstore-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aerkoc_bookstore-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07d76178e3cc8fe9bfb666b9b1700f46c7b36fb42fe95ea3b06d77c04686eda9
|
|
| MD5 |
371c013ef1825d6b8a1b5ba7da95f15a
|
|
| BLAKE2b-256 |
4746badfe26f567e131c020346f0ce19d7a8d76957b4aa7d044c3a1a7033de1f
|
Provenance
The following attestation bundles were made for aerkoc_bookstore-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on sly01/bookstore
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aerkoc_bookstore-0.1.0-py3-none-any.whl -
Subject digest:
07d76178e3cc8fe9bfb666b9b1700f46c7b36fb42fe95ea3b06d77c04686eda9 - Sigstore transparency entry: 1805069516
- Sigstore integration time:
-
Permalink:
sly01/bookstore@81d274e053357c1d9eb6e6b8d019822bf1761e68 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sly01
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81d274e053357c1d9eb6e6b8d019822bf1761e68 -
Trigger Event:
push
-
Statement type: