From 1af07f96ba9f90e234467e5639440653c9df8d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= Date: Mon, 15 Dec 2025 10:28:07 +0100 Subject: [PATCH 1/8] Adjust docstring --- clive/__private/settings/_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clive/__private/settings/_settings.py b/clive/__private/settings/_settings.py index 553652c6a8..63a4239ce2 100644 --- a/clive/__private/settings/_settings.py +++ b/clive/__private/settings/_settings.py @@ -50,7 +50,7 @@ class Settings: The environment variable ```bash - CLIVE__FIRST_GROUP__NESTED_GROUP__SOME_KEY=124 + CLIVE_FIRST_GROUP__NESTED_GROUP__SOME_KEY=124 ``` would override the setting in the file. -- GitLab From 69919a0f578ee0174f3a05d3c5d3765b54214cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= Date: Thu, 11 Dec 2025 09:16:32 +0000 Subject: [PATCH 2/8] Add CLAUDE.md with project documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive project overview and architecture documentation - Document CLI and TUI structure with reference to docs/cli_commands_structure.md - Include testing patterns, fixtures, and CI configuration - Document code style, conventions, and development guidelines - Add known accounts, tracked accounts, and profile system details - Reference configuration sources instead of hardcoded values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..b59dfa3eb0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,295 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Clive** is a CLI and TUI (Terminal User Interface) application for interacting with the Hive blockchain. It's written +in Python and designed to replace the original Hive CLI for power users and testing. The TUI features mouse-based +navigation inspired by midnight commander. + +- **Main entry point**: `clive` - automatically launches TUI when run without arguments, or CLI mode when arguments + are provided like `clive` for TUI and `clive --help` for CLI +- **Development entry point**: `clive-dev` - includes extra debugging information +- **Python version**: (see `requires-python` in `pyproject.toml`, restricted due to wax dependency) +- **Build system**: Poetry +- **Main branch**: `develop` (use this for PRs, not `main`) + +## GitLab Instance + +This project uses **gitlab.syncad.com**, NOT gitlab.com. + +- Repository: https://gitlab.syncad.com/hive/clive +- Use `glab api "projects/hive%2Fclive/..."` for API calls + +## Essential Commands + +### Installation and Setup + +```bash +# Install dependencies (must be run from repository root) +poetry install + +# When updating dependencies and hive submodule is updated also, test-tools should be forced to uninstall first +pip uninstall -y test-tools && poetry install +``` + +### Running Clive + +```bash +# Launch TUI (default) +clive + +# Use CLI mode +clive --help +clive show profile +clive configure profile create + +# Development mode with debug info (useful for presenting full stack-trace instead of pretty errors in CLI) +clive-dev +``` + +### Linting and Formatting + +```bash +# Run all pre-commit hooks (include tools like ruff, mypy and additional hooks) +pre-commit run --all-files + +# Lint with Ruff +ruff check clive/ tests/ + +# Format code +ruff format clive/ tests/ + +# Type checking +mypy clive/ tests/ +``` + +### Testing + +```bash +# Run smoke test +pytest -n 2 tests/functional/cli/show/test_show_account.py::test_show_account tests/functional/cli/process/test_process_transfer.py::test_process_transfer + +# Run all tests in parallel (default process count set in .gitlab-ci.yml) +pytest -n 16 + +# Run unit tests only +pytest tests/unit/ + +# Run functional tests (CLI or TUI) +pytest tests/functional/cli/ +pytest tests/functional/tui/ + +# Run a single test file (example) +pytest tests/unit/test_date_utils.py + +# Run a specific test (example) +pytest tests/unit/test_date_utils.py::test_specific_function -v + +# Run with timeout (important for tests that may hang) +pytest --timeout=600 + +# Run without parallelization (for debugging) +pytest -n 0 +``` + +**Note**: Tests require the embedded testnet dependencies. Some tests spawn local Hive nodes using `test-tools`. + +## Architecture + +### Configuration + +**Global settings** can be configured via: + +1. **Settings files** (in order of precedence): + + - `~/.clive/settings.toml` (user settings, higher priority) + - `{project_root}/settings.toml` (project defaults) + +2. **Environment variables** override settings files using the format: + + ```bash + CLIVE_{GROUP}__{KEY}=value + ``` + + Example: `CLIVE_NODE__CHAIN_ID=abc123` overrides `[NODE] CHAIN_ID` in settings.toml + +3. **Special environment variable**: `CLIVE_DATA_PATH` controls the Clive data directory (default: `~/.clive`). This + also determines where user settings are loaded from (`$CLIVE_DATA_PATH/settings.toml`). + +**Per-profile settings** are stored separately for each profile and configured via `clive configure`: + +- Tracked accounts (working account + watched accounts) +- Known accounts (and enable/disable feature) +- Key aliases +- Node address +- Chain ID + +See `clive configure --help` for all available options. + +### Core Architecture Pattern: Command Pattern + +Clive uses a **Command Pattern** for all operations that interact with the blockchain or beekeeper: + +- **Commands location**: `clive/__private/core/commands/` +- **Base classes**: All commands inherit from `Command` (in `abc/command.py`) +- **Execution**: Commands are async and executed via `await command.execute()` +- **Command hierarchy**: + - `Command` - Base class with `_execute()` method + - `CommandWithResult` - Commands that return a value + - `CommandRestricted` - Base for commands with execution preconditions + - `CommandInUnlocked` - Commands requiring unlocked user wallet + - `CommandEncryption` - Commands requiring both unlocked user wallet and encryption wallet + - `CommandPasswordSecured` - Commands requiring a password + - `CommandDataRetrieval` - Commands that fetch data from the node + - `CommandCachedDataRetrieval` - Data retrieval with caching support + +### World Object - Application Container + +`World` (`clive/__private/core/world.py`) is the top-level container and single source of truth: + +- `world.profile` - Current user profile (settings, accounts, keys) +- `world.node` - Hive node connection for API calls +- `world.commands` - Access to all command instances +- `world.beekeeper_manager` - Manages beekeeper (key storage) lifecycle +- `world.app_state` - Application state (locked/unlocked, etc.) + +**Important**: Direct `world.node` API calls should be avoided in CLI/TUI. Use `world.commands` instead, which handles +errors properly. + +### Profile System + +Profiles (`clive/__private/core/profile.py`) store user configuration: + +- **Working account**: The currently active Hive account +- **Watched accounts**: Accounts being monitored +- **Known accounts**: Accounts approved for transactions. CLI requires explicit addition before broadcasting + operations (configurable via `enable`/`disable`). TUI automatically adds accounts when operations are added to cart + (also configurable). Managed via `clive configure known-account` +- **Key aliases**: Named public keys +- **Transaction**: Pending transaction operations +- **Node address**: Hive node endpoint +- **Chain ID**: Blockchain identifier (like a mainnet/mirrornet/testnet) + +Profiles are persisted to disk via `PersistentStorageService` with encryption support. + +**Note**: Tracked accounts is a combination of working account and watched accounts. + +### Dual Interface Architecture + +**CLI Mode** (`clive/__private/cli/`): + +- Built with **Typer** for command-line interface +- Main command groups: `configure`, `show`, `process`, `beekeeper`, `generate`, `unlock`, `lock` +- For complete CLI command structure, see `docs/cli_commands_structure.md` +- CLI implementation in `clive/__private/cli/` +- Most of the commands —especially those that interact with profile— require Beekeeper (via the + `CLIVE_BEEKEEPER__REMOTE_ADDRESS` and `CLIVE_BEEKEEPER__SESSION_TOKEN` environment variables) for profile encryption + and decryption. +- Commands `clive beekeeper spawn` and `clive beekeeper create-session` can be used for preparing the CLI environment. + +**TUI Mode** (`clive/__private/ui/`): + +- Built with **Textual** (Python TUI framework) +- Main app: `clive/__private/ui/app.py` (Clive class) +- Screens in `clive/__private/ui/screens/` +- Widgets in `clive/__private/ui/widgets/` +- Styling: TCSS (a Textual variation of CSS) files stored as .scss due to better syntax highlighting +- TUI can be used in environment where Beekeeper is already running (`CLIVE_BEEKEEPER__REMOTE_ADDRESS` and + `CLIVE_BEEKEEPER__SESSION_TOKEN` env vars are set), but without them, beekeeper will be automatically spawned and + session will be created when starting TUI. + +### Beekeeper Integration + +Clive uses **beekeepy** (async Python wrapper) to communicate with Hive's beekeeper for key management: + +- **BeekeeperManager**: `clive/__private/core/beekeeper_manager.py` +- Beekeeper stores keys in encrypted wallets +- Beekeeper wallets are stored in the `~/.clive/beekeeper` directory (or `$CLIVE_DATA_PATH/beekeeper` if customized) +- Two wallet types: user wallets (for signing) and encryption wallets (for encrypting profile data) +- Wallets must be unlocked before use +- Beekeeper address and session token can be pointed with respective setting in the `settings.toml` file or via env + var that would have higher precedence + +### Blockchain Communication + +- **Node interaction**: `clive/__private/core/node/node.py` +- **API wrapper**: `clive/__private/core/node/async_hived/` - async wrapper around Hive node APIs +- **Wax integration**: Uses `hiveio-wax` for transaction building and signing +- **Operation models**: `clive/__private/models/schemas.py` - Pydantic models for Hive operations + +### Storage and Migrations + +- **Storage service**: `clive/__private/storage/service/service.py` +- **Converters**: Runtime models ↔ Storage models +- **Migrations**: `clive/__private/storage/migrations/` - versioned profile schema migrations +- Profiles are stored as encrypted files (location can controlled by `CLIVE_DATA_PATH` environment variable, default: + `~/.clive/data/`) + +## Test Organization + +Tests are organized into two main categories: + +- **`tests/unit/`** - Unit tests for individual components (keys, storage, commands, etc.) +- **`tests/functional/`** - Functional tests split by interface: + - `functional/cli/` - CLI command tests + - `functional/tui/` - TUI interaction tests + +**Test fixtures and patterns**: + +Common fixtures (`tests/conftest.py`): + +- `world` - Async World instance +- `beekeeper` - Async beekeeper instance from beekeepy (spawned automatically) +- `node` - Local testnet node (spawned automatically via test-tools) + +CLI test patterns (`tests/functional/cli/`): + +- Uses `CLITester` from `clive-local-tools` package +- `cli_tester` fixture - Provides typed CLI testing interface with command invocation and output checking + +TUI test patterns (`tests/functional/tui/`): + +- Uses `ClivePilot` (Textual's async test driver) +- `prepared_env` fixture - Returns `(node, wallet, pilot)` tuple with TUI ready on Unlock screen +- `prepared_tui_on_dashboard` fixture - TUI already authenticated and on Dashboard screen +- `node_with_wallet` fixture - Test node with initialized wallet +- Tests interact with TUI via pilot (e.g., `pilot.click()`, `pilot.press()`) + +## Development Guidelines + +### Code Style + +- **Strict mypy**: Type hints are required and strictly enforced +- **Ruff**: Comprehensive linting with "ALL" rules (see `pyproject.toml` for ignored rules) +- **Future imports**: All files must have `from __future__ import annotations` (enforced by ruff) +- **Docstrings**: Google style, checked by pydoclint (no redundant type hints in docstrings) +- **Line length**: See `line-length` in `pyproject.toml` (currently 120 characters) + +### Important Conventions + +1. **Private modules**: Implementation details are in `__private/` directories +2. **No direct initialization**: Some classes (like `Profile`) use factory methods instead of `__init__` +3. **Command pattern**: Always use commands for blockchain operations, not direct API calls +4. **Async context managers**: Many resources (World, Node) require async context manager usage +5. **Settings**: Use `safe_settings` from `clive/__private/settings` for reading configuration + +### When Working With Tests + +- Pytest tests use `test-tools` from the Hive submodule for spawning local nodes +- For manual tests, local testnet node can be started manually via `testnet_node.py` +- Both `testnet_node.py` and `test-tools` based tests require executables from the `hive` submodule pointed by the + `HIVE_BUILD_ROOT_PATH` environment variable +- Beekeeper is spawned automatically by test fixtures when needed +- Tests modify settings to use test directories, not user's actual Clive data +- The `clive-local-tools` package provides test helpers and checkers + +### CI Environment + +Tests run in CI with: + +- Parallel processes configurable via `PYTEST_NUMBER_OF_PROCESSES` (see `.gitlab-ci.yml`, default: 16) +- Timeout configurable via `PYTEST_TIMEOUT_MINUTES` (typically 10 minutes for most test suites) +- Separate jobs for unit tests, CLI tests, and TUI tests +- Tests run against installed wheel (not editable install) -- GitLab From 2edd1a70207917bc3c84899d380420e10ce43b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= Date: Mon, 15 Dec 2025 10:53:51 +0000 Subject: [PATCH 3/8] Add Claude Code slash commands for common workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add /smoke, /lint, /test, and /reflection commands to streamline development workflows like running smoke tests, linting, and pytest. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/commands/lint.md | 13 +++++++++ .claude/commands/reflection.md | 48 ++++++++++++++++++++++++++++++++++ .claude/commands/smoke.md | 9 +++++++ .claude/commands/test.md | 28 ++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 .claude/commands/lint.md create mode 100644 .claude/commands/reflection.md create mode 100644 .claude/commands/smoke.md create mode 100644 .claude/commands/test.md diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md new file mode 100644 index 0000000000..dc7ec82026 --- /dev/null +++ b/.claude/commands/lint.md @@ -0,0 +1,13 @@ +Run all linting and formatting checks on the codebase. + +Execute this command: + +```bash +pre-commit run --all-files +``` + +Report the results: + +- If all checks pass, confirm success +- If checks fail, summarize which hooks failed and the key issues found +- For auto-fixable issues (like formatting), mention if files were modified diff --git a/.claude/commands/reflection.md b/.claude/commands/reflection.md new file mode 100644 index 0000000000..5368d98e52 --- /dev/null +++ b/.claude/commands/reflection.md @@ -0,0 +1,48 @@ +You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to +analyze and improve the instructions for Claude Code. Follow these steps carefully: + +1. Analysis Phase: Review the chat history in your context window. + +Then, examine the current Claude instructions, commands and config /CLAUDE.md /.claude/commands/\* +\*\*/CLAUDE.md .claude/settings.json .claude/settings.local.json + +Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for: + +- Inconsistencies in Claude's responses +- Misunderstandings of user requests +- Areas where Claude could provide more detailed or accurate information +- Opportunities to enhance Claude's ability to handle specific types of queries or tasks +- New commands or improvements to a commands name, function or response +- MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them + for the command to work + +2. Interaction Phase: Present your findings and improvement ideas to the human. For each suggestion: a) Explain the + current issue you've identified b) Propose a specific change or addition to the instructions c) Describe how this + change would improve Claude's performance + +Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the +implementation phase. If not, refine your suggestion or move on to the next idea. + +3. Implementation Phase: For each approved change: a) Clearly state the section of the instructions you're modifying b) + Present the new or modified text for that section c) Explain how this change addresses the issue identified in the + analysis phase + +4. Output Format: Present your final output in the following structure: + + +[List the issues identified and potential improvements] + + + +[For each approved improvement: +1. Section being modified +2. New or modified instruction text +3. Explanation of how this addresses the identified issue] + + + [Present the complete, updated set of instructions for Claude, incorporating all approved changes] + + +Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and +purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your +implementations. diff --git a/.claude/commands/smoke.md b/.claude/commands/smoke.md new file mode 100644 index 0000000000..10230b31e8 --- /dev/null +++ b/.claude/commands/smoke.md @@ -0,0 +1,9 @@ +Run the smoke test to quickly verify basic CLI functionality. + +Execute this command: + +```bash +pytest -n 2 tests/functional/cli/show/test_show_account.py::test_show_account tests/functional/cli/process/test_process_transfer.py::test_process_transfer +``` + +Report the results concisely - whether tests passed or failed, and any errors encountered. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 0000000000..0ec397fc32 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,28 @@ +Run pytest with the provided arguments. + +Arguments: $ARGUMENTS + +First, expand any shortcuts in the arguments: + +- `unit` → `tests/unit/` +- `cli` → `tests/functional/cli/` +- `tui` → `tests/functional/tui/` +- `functional` → `tests/functional/` + +Then execute pytest with the expanded path. Examples: + +- `/test unit` runs `pytest tests/unit/` +- `/test cli` runs `pytest tests/functional/cli/` +- `/test tests/unit/test_date_utils.py -v` runs as-is (full path provided) + +If no arguments provided, run all tests with parallel execution: + +```bash +pytest -n 16 +``` + +Report results concisely: + +- Number of tests passed/failed/skipped +- For failures, show the test name and a brief summary of the error +- Suggest next steps if tests fail -- GitLab From f28af530361d66db6ecccb16864a2420b65add6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= Date: Mon, 15 Dec 2025 11:23:23 +0000 Subject: [PATCH 4/8] Document Claude Code slash commands and simplify Testing section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add new "Claude Code Commands" section documenting /smoke, /lint, /test, /reflection - Document test shortcuts (unit, cli, tui, functional) - Simplify Testing section by referencing slash commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b59dfa3eb0..9c71840cdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,17 @@ This project uses **gitlab.syncad.com**, NOT gitlab.com. - Repository: https://gitlab.syncad.com/hive/clive - Use `glab api "projects/hive%2Fclive/..."` for API calls +## Claude Code Commands + +Available slash commands for development workflows: + +| Command | Description | +| -------------- | ------------------------------------------------------------- | +| `/smoke` | Run smoke test | +| `/lint` | Run all pre-commit hooks | +| `/test ` | Run pytest with shortcuts: `unit`, `cli`, `tui`, `functional` | +| `/reflection` | Analyze and improve Claude Code configuration | + ## Essential Commands ### Installation and Setup @@ -67,23 +78,10 @@ mypy clive/ tests/ ### Testing -```bash -# Run smoke test -pytest -n 2 tests/functional/cli/show/test_show_account.py::test_show_account tests/functional/cli/process/test_process_transfer.py::test_process_transfer - -# Run all tests in parallel (default process count set in .gitlab-ci.yml) -pytest -n 16 - -# Run unit tests only -pytest tests/unit/ - -# Run functional tests (CLI or TUI) -pytest tests/functional/cli/ -pytest tests/functional/tui/ - -# Run a single test file (example) -pytest tests/unit/test_date_utils.py +Use `/smoke`, `/lint`, and `/test` slash commands for common workflows (see +[Claude Code Commands](#claude-code-commands)). +```bash # Run a specific test (example) pytest tests/unit/test_date_utils.py::test_specific_function -v -- GitLab From 35484a0434c18e15b458d666c9e3e3854f2303a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= Date: Mon, 15 Dec 2025 13:03:47 +0000 Subject: [PATCH 5/8] Add useful GitLab CLI commands to CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added glab commands for common MR and pipeline operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9c71840cdf..707610c792 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -283,6 +283,24 @@ TUI test patterns (`tests/functional/tui/`): - Tests modify settings to use test directories, not user's actual Clive data - The `clive-local-tools` package provides test helpers and checkers +### Useful Commands + +#### GitLab CLI (glab) + +```bash +# Find MR for a branch +glab mr list --source-branch= + +# Add comment to MR +glab mr note --message "..." + +# Get pipeline job details +glab api "projects/hive%2Fclive/pipelines//jobs" + +# Get job logs +glab api "projects/hive%2Fclive/jobs//trace" +``` + ### CI Environment Tests run in CI with: -- GitLab From 63ff95d2694eafae9231a230104b4eb61fedf101 Mon Sep 17 00:00:00 2001 From: Aleksandra Grabowska Date: Fri, 9 Jan 2026 13:43:21 +0000 Subject: [PATCH 6/8] Add account_name parser function for CLI validation --- clive/__private/cli/common/parsers.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/clive/__private/cli/common/parsers.py b/clive/__private/cli/common/parsers.py index 08ad4a9cad..48235f8cdb 100644 --- a/clive/__private/cli/common/parsers.py +++ b/clive/__private/cli/common/parsers.py @@ -143,3 +143,30 @@ def public_key(raw: str) -> PublicKey: except PublicKeyInvalidFormatError as error: raise CLIPublicKeyInvalidFormatError(raw) from error return parsed + + +def account_name(raw: str) -> str: + """ + Parse and validate account name. + + Args: + raw: The raw input representing the account name. + + Raises: + typer.BadParameter: If the account name is invalid. + + Returns: + The validated account name. + """ + from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD # noqa: PLC0415 + from clive.__private.core.formatters.humanize import humanize_validation_result # noqa: PLC0415 + from clive.__private.validators.account_name_validator import AccountNameValidator # noqa: PLC0415 + + # Allow placeholder value to pass through without validation + if raw == PERFORM_WORKING_ACCOUNT_LOAD: + return raw + + status = AccountNameValidator().validate(raw) + if status.is_valid: + return raw + raise typer.BadParameter(humanize_validation_result(status)) -- GitLab From 09cb4934f6168cfbabb92f19e72df7c026f325e1 Mon Sep 17 00:00:00 2001 From: Aleksandra Grabowska Date: Fri, 9 Jan 2026 13:43:36 +0000 Subject: [PATCH 7/8] Apply account_name parser to CLI parameters --- .../cli/common/parameters/argument_related_options.py | 10 +++++++++- clive/__private/cli/common/parameters/arguments.py | 2 ++ clive/__private/cli/common/parameters/options.py | 7 +++++++ clive/__private/cli/configure/known_account.py | 5 ++++- clive/__private/cli/configure/profile.py | 3 ++- clive/__private/cli/configure/tracked_account.py | 5 ++++- clive/__private/cli/configure/working_account.py | 2 ++ clive/__private/cli/generate/main.py | 2 ++ .../cli/process/custom_operations/custom_json.py | 2 ++ clive/__private/cli/process/hive_power/delegations.py | 2 ++ clive/__private/cli/process/main.py | 6 ++++-- clive/__private/cli/process/proxy.py | 3 ++- clive/__private/cli/process/update_authority.py | 2 ++ clive/__private/cli/process/vote_witness.py | 5 +++-- clive/__private/cli/show/main.py | 5 ++++- 15 files changed, 51 insertions(+), 10 deletions(-) diff --git a/clive/__private/cli/common/parameters/argument_related_options.py b/clive/__private/cli/common/parameters/argument_related_options.py index 40537daa3e..b5e47778b3 100644 --- a/clive/__private/cli/common/parameters/argument_related_options.py +++ b/clive/__private/cli/common/parameters/argument_related_options.py @@ -13,6 +13,7 @@ import typer from clive.__private.cli.common.parameters import options from clive.__private.cli.common.parameters.modified_param import modified_param +from clive.__private.cli.common.parsers import account_name as account_name_parser from clive.__private.cli.common.parsers import public_key from clive.__private.core.constants.cli import LOOK_INTO_ARGUMENT_OPTION_HELP @@ -41,7 +42,14 @@ profile_name = _make_argument_related_option(options.profile_name) new_account_name = _make_argument_related_option(options.new_account_name) -name = _make_argument_related_option("--name") +name = _make_argument_related_option( + typer.Option( + None, + "--name", + parser=account_name_parser, + help=LOOK_INTO_ARGUMENT_OPTION_HELP, + ) +) key = _make_argument_related_option("--key") diff --git a/clive/__private/cli/common/parameters/arguments.py b/clive/__private/cli/common/parameters/arguments.py index 32445eb450..96f5305dd4 100644 --- a/clive/__private/cli/common/parameters/arguments.py +++ b/clive/__private/cli/common/parameters/arguments.py @@ -12,11 +12,13 @@ import typer from clive.__private.cli.common.parameters import modified_param from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name as account_name_parser from clive.__private.cli.common.parsers import public_key from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD working_account_template = typer.Argument( PERFORM_WORKING_ACCOUNT_LOAD, # we don't know if account_name_option is required until the profile is loaded + parser=account_name_parser, help=stylized_help("The account to use.", is_working_account_default=True, required_as_arg_or_option=True), show_default=False, ) diff --git a/clive/__private/cli/common/parameters/options.py b/clive/__private/cli/common/parameters/options.py index c957e38ed1..fc7838152c 100644 --- a/clive/__private/cli/common/parameters/options.py +++ b/clive/__private/cli/common/parameters/options.py @@ -14,6 +14,9 @@ import typer from clive.__private.cli.common.parameters.modified_param import modified_param from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import ( + account_name as account_name_parser, +) from clive.__private.cli.common.parsers import ( decimal_percent, liquid_asset, @@ -28,12 +31,14 @@ from clive.__private.core.constants.cli import ( working_account_template = typer.Option( PERFORM_WORKING_ACCOUNT_LOAD, # we don't know if account_name_option is required until the profile is loaded + parser=account_name_parser, help=stylized_help("The account to use.", is_working_account_default=True), show_default=False, ) working_account_list_template = typer.Option( [PERFORM_WORKING_ACCOUNT_LOAD], # we don't know if account_name_option is required until the profile is loaded + parser=account_name_parser, help=stylized_help("List of accounts to use.", is_working_account_default=True), show_default=False, ) @@ -49,6 +54,7 @@ account_name = modified_param(working_account_template, param_decls=("--account- new_account_name = typer.Option( ..., "--new-account-name", + parser=account_name_parser, help="The name of the new account.", ) @@ -67,6 +73,7 @@ to_account_name = modified_param( to_account_name_required = typer.Option( ..., "--to", + parser=account_name_parser, help='The account to use as "to" argument.', ) diff --git a/clive/__private/cli/configure/known_account.py b/clive/__private/cli/configure/known_account.py index 3825bbcb6b..04b7435762 100644 --- a/clive/__private/cli/configure/known_account.py +++ b/clive/__private/cli/configure/known_account.py @@ -6,11 +6,14 @@ from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import argument_related_options, modified_param from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name known_account = CliveTyper(name="known-account", help="Manage your known account(s).") _account_name_add_argument = typer.Argument( - None, help=stylized_help("The name of the known account to add.", required_as_arg_or_option=True) + None, + parser=account_name, + help=stylized_help("The name of the known account to add.", required_as_arg_or_option=True), ) diff --git a/clive/__private/cli/configure/profile.py b/clive/__private/cli/configure/profile.py index 692a8e5438..bf0652ae99 100644 --- a/clive/__private/cli/configure/profile.py +++ b/clive/__private/cli/configure/profile.py @@ -6,6 +6,7 @@ from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common.parameters import argument_related_options, modified_param from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleProfileNameValue from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name profile = CliveTyper(name="profile", help="Manage your Clive profile(s).") @@ -19,7 +20,7 @@ _profile_name_create_argument = typer.Argument( async def create_profile( profile_name: str | None = _profile_name_create_argument, profile_name_option: str | None = argument_related_options.profile_name, - working_account_name: str | None = typer.Option(None, help="The name of the working account."), + working_account_name: str | None = typer.Option(None, parser=account_name, help="The name of the working account."), ) -> None: """ Create a new profile. Password for new profile is provided by stdin. diff --git a/clive/__private/cli/configure/tracked_account.py b/clive/__private/cli/configure/tracked_account.py index 6c84c00c3f..0e90bc5698 100644 --- a/clive/__private/cli/configure/tracked_account.py +++ b/clive/__private/cli/configure/tracked_account.py @@ -6,11 +6,14 @@ from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import argument_related_options, modified_param from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name tracked_account = CliveTyper(name="tracked-account", help="Manage your tracked account(s).") _account_name_add_argument = typer.Argument( - None, help=stylized_help("The name of the tracked account to add.", required_as_arg_or_option=True) + None, + parser=account_name, + help=stylized_help("The name of the tracked account to add.", required_as_arg_or_option=True), ) diff --git a/clive/__private/cli/configure/working_account.py b/clive/__private/cli/configure/working_account.py index fcc648acb7..67054af505 100644 --- a/clive/__private/cli/configure/working_account.py +++ b/clive/__private/cli/configure/working_account.py @@ -6,11 +6,13 @@ from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common.parameters import argument_related_options from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name working_account = CliveTyper(name="working-account", help="Manage your working account.") _account_name_switch_argument = typer.Argument( None, + parser=account_name, help=stylized_help("The name of the account to switch to.", required_as_arg_or_option=True), ) diff --git a/clive/__private/cli/generate/main.py b/clive/__private/cli/generate/main.py index e0cfbfc78e..d033315317 100644 --- a/clive/__private/cli/generate/main.py +++ b/clive/__private/cli/generate/main.py @@ -9,6 +9,7 @@ from clive.__private.cli.common.parameters.argument_related_options import ( ) from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleValue from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name from clive.__private.core.types import AuthorityLevel generate = CliveTyper(name="generate", help="Commands for generating things (e.g. keys).") @@ -16,6 +17,7 @@ generate = CliveTyper(name="generate", help="Commands for generating things (e.g _account_name_argument = typer.Argument( None, + parser=account_name, help=stylized_help( "Account for which key is derived, this is not working account of profile.", required_as_arg_or_option=True ), diff --git a/clive/__private/cli/process/custom_operations/custom_json.py b/clive/__private/cli/process/custom_operations/custom_json.py index 16e4fcad09..317f597d86 100644 --- a/clive/__private/cli/process/custom_operations/custom_json.py +++ b/clive/__private/cli/process/custom_operations/custom_json.py @@ -4,6 +4,7 @@ import typer from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import modified_param, options +from clive.__private.cli.common.parsers import account_name custom_json = CliveTyper(name="custom-json", help="Send raw custom json operation.") @@ -18,6 +19,7 @@ async def process_custom_json( # noqa: PLR0913 ), authorize_by_active: list[str] = typer.Option( [], + parser=account_name, help="Active authorities. Option can be added multiple times. If neither authorize nor authorize-by-active is" " used, then posting authority of working account is used for authorization.", ), diff --git a/clive/__private/cli/process/hive_power/delegations.py b/clive/__private/cli/process/hive_power/delegations.py index e32b235f29..10ff290be5 100644 --- a/clive/__private/cli/process/hive_power/delegations.py +++ b/clive/__private/cli/process/hive_power/delegations.py @@ -6,6 +6,7 @@ import typer from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import options +from clive.__private.cli.common.parsers import account_name if TYPE_CHECKING: from clive.__private.models.asset import Asset @@ -15,6 +16,7 @@ delegations = CliveTyper(name="delegations", help="Set or remove vesting delegat _delegatee_account_name = typer.Option( ..., "--delegatee", + parser=account_name, help='The account to use as "delegatee" argument.', ) diff --git a/clive/__private/cli/process/main.py b/clive/__private/cli/process/main.py index 4a54635bca..9b8cb089a9 100644 --- a/clive/__private/cli/process/main.py +++ b/clive/__private/cli/process/main.py @@ -13,7 +13,7 @@ from clive.__private.cli.common.parameters.ensure_single_value import ( EnsureSingleValue, ) from clive.__private.cli.common.parameters.styling import stylized_help -from clive.__private.cli.common.parsers import decimal_percent, hbd_asset, hive_asset, public_key +from clive.__private.cli.common.parsers import account_name, decimal_percent, hbd_asset, hive_asset, public_key from clive.__private.cli.process.claim import claim from clive.__private.cli.process.custom_operations.custom_json import custom_json from clive.__private.cli.process.hive_power.delegations import delegations @@ -56,7 +56,7 @@ process.add_typer(voting_rights) @process.command(name="transfer") async def transfer( # noqa: PLR0913 from_account: str = options.from_account_name, - to: str = typer.Option(..., help="The account to transfer to."), + to: str = typer.Option(..., parser=account_name, help="The account to transfer to."), amount: str = options.liquid_amount, memo: str = options.memo_text, sign_with: str | None = options.sign_with, @@ -145,6 +145,7 @@ async def process_update_memo_key( # noqa: PLR0913 _new_account_name_argument = typer.Argument( None, + parser=account_name, help=stylized_help("The name of the new account.", required_as_arg_or_option=True), ) @@ -303,6 +304,7 @@ async def process_change_recovery_account( # noqa: PLR0913 ), new_recovery_account: str = typer.Option( ..., + parser=account_name, help="This is your trusted account. In case of compromise, only this account can create a recovery request.", ), sign_with: str | None = options.sign_with, diff --git a/clive/__private/cli/process/proxy.py b/clive/__private/cli/process/proxy.py index ffa184b6c1..c6129106c4 100644 --- a/clive/__private/cli/process/proxy.py +++ b/clive/__private/cli/process/proxy.py @@ -4,6 +4,7 @@ import typer from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import options +from clive.__private.cli.common.parsers import account_name proxy = CliveTyper(name="proxy", help="Set, change or remove a proxy.") @@ -11,7 +12,7 @@ proxy = CliveTyper(name="proxy", help="Set, change or remove a proxy.") @proxy.command(name="set") async def process_proxy_set( # noqa: PLR0913 account_name: str = options.account_name, - proxy: str = typer.Option(..., help="Name of new proxy account."), + proxy: str = typer.Option(..., parser=account_name, help="Name of new proxy account."), sign_with: str | None = options.sign_with, autosign: bool | None = options.autosign, # noqa: FBT001 broadcast: bool | None = options.broadcast, # noqa: FBT001 diff --git a/clive/__private/cli/process/update_authority.py b/clive/__private/cli/process/update_authority.py index 4c107c9557..03c333371a 100644 --- a/clive/__private/cli/process/update_authority.py +++ b/clive/__private/cli/process/update_authority.py @@ -9,6 +9,7 @@ from click import Context, pass_context from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import options from clive.__private.cli.common.parameters import modified_param +from clive.__private.cli.common.parsers import account_name from clive.__private.core._async import asyncio_run if TYPE_CHECKING: @@ -20,6 +21,7 @@ if TYPE_CHECKING: _authority_account_name = typer.Option( ..., "--account", + parser=account_name, help="The account to add/remove/modify (account must exist).", ) _authority_key = typer.Option( diff --git a/clive/__private/cli/process/vote_witness.py b/clive/__private/cli/process/vote_witness.py index fa5fc9dce2..1c29ca4f76 100644 --- a/clive/__private/cli/process/vote_witness.py +++ b/clive/__private/cli/process/vote_witness.py @@ -4,6 +4,7 @@ import typer from clive.__private.cli.clive_typer import CliveTyper from clive.__private.cli.common import options +from clive.__private.cli.common.parsers import account_name vote_witness = CliveTyper(name="vote-witness", help="Vote/unvote for a witness.") @@ -11,7 +12,7 @@ vote_witness = CliveTyper(name="vote-witness", help="Vote/unvote for a witness." @vote_witness.command(name="add") async def process_vote_witness_add( # noqa: PLR0913 account_name: str = options.account_name, - witness_name: str = typer.Option(..., help="Witness name to vote."), + witness_name: str = typer.Option(..., parser=account_name, help="Witness name to vote."), sign_with: str | None = options.sign_with, autosign: bool | None = options.autosign, # noqa: FBT001 broadcast: bool | None = options.broadcast, # noqa: FBT001 @@ -34,7 +35,7 @@ async def process_vote_witness_add( # noqa: PLR0913 @vote_witness.command(name="remove") async def process_vote_witness_remove( # noqa: PLR0913 account_name: str = options.account_name, - witness_name: str = typer.Option(..., help="Witness name to unvote."), + witness_name: str = typer.Option(..., parser=account_name, help="Witness name to unvote."), sign_with: str | None = options.sign_with, autosign: bool | None = options.autosign, # noqa: FBT001 broadcast: bool | None = options.broadcast, # noqa: FBT001 diff --git a/clive/__private/cli/show/main.py b/clive/__private/cli/show/main.py index 9e4fc1c8e8..9808d8ab5e 100644 --- a/clive/__private/cli/show/main.py +++ b/clive/__private/cli/show/main.py @@ -10,6 +10,7 @@ from clive.__private.cli.common.parameters.ensure_single_value import ( ) from clive.__private.cli.common.parameters.modified_param import modified_param from clive.__private.cli.common.parameters.styling import stylized_help +from clive.__private.cli.common.parsers import account_name from clive.__private.cli.show.pending import pending from clive.__private.core.constants.data_retrieval import ( ORDER_DIRECTION_DEFAULT, @@ -130,7 +131,9 @@ async def show_witnesses( ).run() -_witness_name_argument = typer.Argument(None, help=stylized_help("Witness name.", required_as_arg_or_option=True)) +_witness_name_argument = typer.Argument( + None, parser=account_name, help=stylized_help("Witness name.", required_as_arg_or_option=True) +) @show.command(name="witness") -- GitLab From d7a811889fedcbfbdadc5e442641d90782c091ae Mon Sep 17 00:00:00 2001 From: Aleksandra Grabowska Date: Fri, 9 Jan 2026 13:44:09 +0000 Subject: [PATCH 8/8] Add unit tests for account_name parser --- tests/unit/cli/__init__.py | 0 tests/unit/cli/test_parsers.py | 107 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/unit/cli/__init__.py create mode 100644 tests/unit/cli/test_parsers.py diff --git a/tests/unit/cli/__init__.py b/tests/unit/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/cli/test_parsers.py b/tests/unit/cli/test_parsers.py new file mode 100644 index 0000000000..b0e71bb7e8 --- /dev/null +++ b/tests/unit/cli/test_parsers.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import pytest +import typer + +from clive.__private.cli.common.parsers import account_name +from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD + + +def test_account_name_valid_simple() -> None: + # ARRANGE + raw = "alice" + + # ACT + result = account_name(raw) + + # ASSERT + assert result == "alice" + + +def test_account_name_valid_with_numbers() -> None: + # ARRANGE + raw = "alice123" + + # ACT + result = account_name(raw) + + # ASSERT + assert result == "alice123" + + +def test_account_name_valid_with_dots() -> None: + # ARRANGE + raw = "alice.bob" + + # ACT + result = account_name(raw) + + # ASSERT + assert result == "alice.bob" + + +def test_account_name_valid_with_hyphens() -> None: + # ARRANGE + raw = "alice-bob" + + # ACT + result = account_name(raw) + + # ASSERT + assert result == "alice-bob" + + +def test_account_name_allows_perform_working_account_load_placeholder() -> None: + # ARRANGE + raw = PERFORM_WORKING_ACCOUNT_LOAD + + # ACT + result = account_name(raw) + + # ASSERT + assert result == PERFORM_WORKING_ACCOUNT_LOAD + + +def test_account_name_invalid_too_short() -> None: + # ARRANGE + raw = "ab" + + # ACT & ASSERT + with pytest.raises(typer.BadParameter): + account_name(raw) + + +def test_account_name_invalid_too_long() -> None: + # ARRANGE + raw = "a" * 17 + + # ACT & ASSERT + with pytest.raises(typer.BadParameter): + account_name(raw) + + +def test_account_name_invalid_uppercase() -> None: + # ARRANGE + raw = "Alice" + + # ACT & ASSERT + with pytest.raises(typer.BadParameter): + account_name(raw) + + +def test_account_name_invalid_starting_with_number() -> None: + # ARRANGE + raw = "1alice" + + # ACT & ASSERT + with pytest.raises(typer.BadParameter): + account_name(raw) + + +def test_account_name_invalid_special_chars() -> None: + # ARRANGE + raw = "alice@bob" + + # ACT & ASSERT + with pytest.raises(typer.BadParameter): + account_name(raw) -- GitLab