Metadata-Version: 2.4
Name: aerkoc-bookstore
Version: 0.1.0
Summary: Book management CLI demonstrating hexagonal architecture
Requires-Python: >=3.9
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```bash
# With uv (recommended)
uv sync --extra dev

# Or with pip
pip install -e ".[dev]"
```

## Run it

```bash
# 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:

```bash
BOOKS_FILE=data/library.json uv run books list
```

## Run tests

```bash
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`):

```yaml
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`](examples/consumer-manage-books.yml).

### Publishing a release

Pushing a version tag triggers the publish workflow automatically:

```bash
git tag v0.1.0
git push origin v0.1.0
```

This builds the wheel and uploads it to [pypi.org/project/aerkoc-bookstore/](https://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:

1. Swap `JsonBookStore` for a `PostgresBookStore` without changing the service
2. In tests, inject a `MagicMock(spec=BookStorePort)` — no file I/O needed
3. The CLI is just a thin shell that parses args and delegates to the service
