Skip to content

Commit 08242e6

Browse files
committed
feat(es): improve Search CLI with new commands and safer index management
Replace elasticsearch-dsl with direct client calls, add `rebuild`, `health`, and `queue` commands, and fix several correctness issues across the CLI. * drop `elasticsearch_dsl.Index` from `index.py` and `snapshot/cli.py`; use `current_search_client.indices.*` with `allow_no_indices=True` and `ignore_unavailable=True` instead * change default index pattern for `open`/`close` commands from `_all` to `*` * add `rero es index rebuild <resource>`: zero-data-loss index migration — creates a fresh dated index, reindexes, shows old/new doc counts, asks for confirmation before switching all aliases and deleting the old index * add `rero es health`: connectivity check for Search, database, and Celery broker; exits 1 if any service is unreachable or in error state * add `rero es queue list/stats/purge`: inspect Celery workers and queue depths; purge a named queue with `--yes-i-know` guard * fix `raise click.Abort` → `raise click.Abort()` in `slm/cli.py` (bare reference did nothing) * fix dead assertions in `tests/test_tasks.py` (missing `assert` keyword) and correct expected output string "name arg" → "named arg" * fix `__all__ = "rero"` → `__all__ = ["rero"]` in `cli/__init__.py` and `cli/utils.py` (string iterates as characters) * render `rero es snapshot list` as a formatted table (name, start time, duration, shards, colour-coded state) instead of raw JSON * add `--names-only` flag to `snapshot list` for scripting use * improve `create_snapshot --wait` output: show state, duration and shard counts instead of the raw JSON blob * add `restore_snapshot --delete` flag: deletes RERO ILS indices before restoring * rename "Elasticsearch"/"OpenSearch" → "Search" throughout docstrings and comments Co-Authored-by: Peter Weber <peter.weber@rero.ch>
1 parent bef4874 commit 08242e6

26 files changed

Lines changed: 971 additions & 219 deletions

CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
3535
* feat(dev): add uv and ruff [#21](https://github.com/rero/rero-invenio-base/pull/21) (by @PascalRepond)
3636
* chore(actions): pypi publish package use poetry [#20](https://github.com/rero/rero-invenio-base/pull/20) (by @PascalRepond)
3737

38-
## [v0.3.1](https://github.com/rero/rero-invenio-base/tree/v0.3.0) (2025-04-16)
38+
## [v0.3.1](https://github.com/rero/rero-invenio-base/tree/v0.3.1) (2025-04-16)
3939

4040
[Full Changelog](https://github.com/rero/rero-invenio-base/compare/v0.3.0...v0.3.1)
4141

@@ -62,16 +62,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
6262

6363
## [v0.2.0](https://github.com/rero/rero-invenio-base/tree/v0.2.0) (2023-01-31)
6464

65-
[Full Changelog](https://github.com/rero/rero-invenio-base/compare/...v0.2.0)
65+
[Full Changelog](https://github.com/rero/rero-invenio-base/compare/v0.1.0...v0.2.0)
6666

6767
**Changes:**
6868

6969
* task: add a generic celery task [\#10](https://github.com/rero/rero-invenio-base/pull/10) (by @jma)
7070

7171
## [v0.1.0](https://github.com/rero/rero-invenio-base/tree/v0.1.0) (2022-09-02)
7272

73-
[Full Changelog](https://github.com/rero/rero-invenio-base/compare/...v0.1.0)
74-
7573
**Changes:**
7674

7775
* cli: adds snapshot lifecycle management commands [\#6](https://github.com/rero/rero-invenio-base/pull/6) (by @rerowep)

CLAUDE.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# rero-invenio-base Claude guide
2+
3+
## Overview
4+
5+
rero-invenio-base is a Python library providing generic backend utilities for RERO Invenio instances. It ships SEARCH CLI management commands, a streaming data export framework, and shared Celery tasks.
6+
7+
**Stack**: Python 3.12–3.14, Flask (Invenio), ElasticSearch 7, Celery
8+
**Package manager**: `uv` with `poethepoet` for task running
9+
10+
## Commands
11+
12+
All commands are run through uv's virtual env with `uv run`.
13+
14+
### Linting and formatting
15+
16+
**IMPORTANT:** After editing files, make sure that there are no errors in the formatting and linting.
17+
18+
```bash
19+
uv run poe lint # ruff check
20+
uv run poe format # ruff format
21+
```
22+
23+
### Setup (done by humans)
24+
25+
Human developers will start the required containers and services on their own terms. Tests require SEARCH to be running.
26+
27+
## Architecture
28+
29+
### Package structure
30+
31+
```text
32+
rero_invenio_base/
33+
├── ext.py # Flask extension (REROInvenioBase)
34+
├── config.py # Default configuration keys
35+
├── cli/
36+
│ ├── shared.py # Shared CLI helpers (abort_if_false)
37+
│ ├── utils.py # check_license, check_json commands
38+
│ └── es/
39+
│ ├── alias.py # `rero es alias` commands
40+
│ ├── index.py # `rero es index` commands (reindex, move, update-mapping, …)
41+
│ ├── task.py # `rero es task` commands
42+
│ ├── snapshot/ # `rero es snapshot` commands
43+
│ └── slm/ # `rero es slm` snapshot-management commands
44+
└── modules/
45+
├── tasks.py # run_on_worker Celery task
46+
├── utils.py # chunk() utility
47+
└── export/
48+
├── ext.py # ReroInvenioBaseExportApp Flask extension
49+
├── views.py # ExportResource view + create_blueprint_from_app
50+
└── config.py # Export REST endpoint configuration
51+
```
52+
53+
### Entry points
54+
55+
Registered in `pyproject.toml`:
56+
57+
| Entry point group | Name | Target |
58+
|---|---|---|
59+
| `flask.commands` | `rero` | `rero_invenio_base.cli:rero` |
60+
| `invenio_base.apps` | `rero-invenio-base-export` | `ReroInvenioBaseExportApp` |
61+
| `invenio_base.api_blueprints` | `rero_ils_exports` | `create_blueprint_from_app` |
62+
| `invenio_celery.tasks` | `rero` | `rero_invenio_base.modules.tasks` |
63+
64+
### Export module
65+
66+
`ExportResource` is a `ContentNegotiatedMethodView` that streams record search results. Routes are registered dynamically from the `RERO_INVENIO_BASE_EXPORT_REST_ENDPOINTS` config key via `create_blueprint_from_app`. Each endpoint maps MIME types to serializers and prepends `/export` to the list route.
67+
68+
## Code style
69+
70+
- Be clear and concise in docstrings; do not over-comment the code.
71+
- Do not use Python type annotations (no `-> str`, `: str`, etc. in signatures).
72+
- Use **Sphinx-style** docstrings (`:param x:`, `:returns:`, `:rtype:`).
73+
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org).
74+
75+
## Testing
76+
77+
- Tests use function-based style (no class-based tests).
78+
- Test fixtures are in `tests/conftest.py`; sample data in `tests/data/`.
79+
- `--doctest-modules` is active — doctests in module files are run by default.
80+
81+
### Running the tests (done by humans)
82+
83+
Human developers run tests from their consoles after starting the SEARCH container:
84+
85+
```bash
86+
uv run ./scripts/test # full suite (lint + pip-audit + pytest with SEARCH)
87+
uv run pytest # pytest only (SEARCH must already be running)
88+
```

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ dev = [
3939

4040
[tool.project.entry-points]
4141
check_json = "rero_invenio_base.cli.utils:check_json"
42-
rero = "rero_invenio_base.cli:rero"
4342

4443
[project.entry-points."flask.commands"]
4544
rero = "rero_invenio_base.cli:rero"

rero_invenio_base/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ def rero():
1717
rero.add_command(utils)
1818
rero.add_command(es)
1919

20-
__all__ = "rero"
20+
__all__ = ["rero"]
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
# SPDX-FileCopyrightText: Fondation RERO+
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

4-
"""Click elasticsearch command-line utilities."""
4+
"""Click Search command-line utilities."""
55

66
import click
77

88
from .alias import alias
9+
from .health import health
910
from .index import index
11+
from .queue import queue
1012
from .slm import slm
1113
from .snapshot import snapshot
1214
from .task import task
1315

1416

1517
@click.group()
1618
def es():
17-
"""Elasticsarch management commands."""
19+
"""Search management commands."""
1820

1921

20-
es.add_command(index)
2122
es.add_command(alias)
23+
es.add_command(health)
24+
es.add_command(index)
25+
es.add_command(queue)
2226
es.add_command(slm)
2327
es.add_command(snapshot)
2428
es.add_command(task)
2529

26-
__all__ = "index"
30+
__all__ = ["es"]

rero_invenio_base/cli/es/alias.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-FileCopyrightText: Fondation RERO+
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

4-
"""Click elasticsearch index command-line utilities."""
4+
"""Click Search index command-line utilities."""
55

66
import json
77

@@ -14,13 +14,13 @@
1414

1515
@click.group()
1616
def alias():
17-
"""Elasticsearch alias commands."""
17+
"""Search alias commands."""
1818

1919

2020
@alias.command("get")
2121
@with_appcontext
2222
def get_alias():
23-
"""Get elasticsearch aliases."""
23+
"""Get Search aliases."""
2424
click.secho(json.dumps(current_search_client.indices.get_alias(), indent=2), fg="green")
2525

2626

@@ -29,7 +29,7 @@ def get_alias():
2929
@click.argument("index")
3030
@click.argument("name")
3131
def put_alias(index, name):
32-
"""Put elasticsearch alias."""
32+
"""Put Search alias."""
3333
try:
3434
click.secho(
3535
json.dumps(current_search_client.indices.put_alias(index, name), indent=2),
@@ -51,7 +51,7 @@ def put_alias(index, name):
5151
prompt="Do you really want to delete an alias?",
5252
)
5353
def delete_alias(index, name):
54-
"""Delete elasticsearch alias."""
54+
"""Delete Search alias."""
5555
try:
5656
click.secho(
5757
json.dumps(current_search_client.indices.delete_alias(index, name), indent=2),

rero_invenio_base/cli/es/health.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# RERO Invenio Base
2+
# Copyright (C) 2025 RERO.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU Affero General Public License as published by
6+
# the Free Software Foundation, version 3 of the License.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU Affero General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU Affero General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
"""Click health-check command-line utility."""
17+
18+
from urllib.parse import urlparse, urlunparse
19+
20+
import click
21+
from flask import current_app
22+
from flask.cli import with_appcontext
23+
from invenio_db import db
24+
from invenio_search import current_search_client
25+
from sqlalchemy import text
26+
27+
28+
@click.command("health")
29+
@with_appcontext
30+
def health():
31+
"""Check connectivity to SEARCH, database, and Redis.
32+
33+
Exits with status 1 if any service is unreachable or in error state.
34+
"""
35+
all_ok = True
36+
37+
# SEARCH
38+
try:
39+
s = current_search_client.cluster.health()
40+
color = {"green": "green", "yellow": "yellow"}.get(s["status"], "red")
41+
label = "OK" if s["status"] != "red" else "ERROR"
42+
detail = f"{s['status'].upper()}{s['number_of_nodes']} node(s), cluster: {s['cluster_name']}"
43+
click.secho(f"{'SEARCH':<12} {label:<7} {detail}", fg=color)
44+
if s["status"] == "red":
45+
all_ok = False
46+
except Exception as err:
47+
click.secho(f"{'SEARCH':<12} {'ERROR':<7} {err}", fg="red")
48+
all_ok = False
49+
50+
# Database
51+
try:
52+
db.session.execute(text("SELECT 1"))
53+
click.secho(f"{'Database':<12} {'OK':<7}", fg="green")
54+
except Exception as err:
55+
click.secho(f"{'Database':<12} {'ERROR':<7} {err}", fg="red")
56+
all_ok = False
57+
58+
# Broker / Redis — use Celery's own connection so any broker type works.
59+
try:
60+
from celery import current_app as celery_app
61+
62+
with celery_app.connection() as conn:
63+
conn.ensure_connection(max_retries=1)
64+
broker_url = current_app.config.get("BROKER_URL") or current_app.config.get("CELERY_BROKER_URL", "")
65+
parsed = urlparse(broker_url)
66+
host = parsed.hostname or ""
67+
netloc = f"{host}:{parsed.port}" if parsed.port else host
68+
safe_url = urlunparse(parsed._replace(netloc=netloc))
69+
click.secho(f"{'Broker':<12} {'OK':<7} {safe_url[:60]}", fg="green")
70+
except Exception as err:
71+
click.secho(f"{'Broker':<12} {'ERROR':<7} {err}", fg="red")
72+
all_ok = False
73+
74+
if not all_ok:
75+
raise SystemExit(1)

0 commit comments

Comments
 (0)