diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b9edd7f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Default reviewers for all changes +* @usecortex/hydradb-maintainers + +# CLI commands +/src/hydradb_cli/commands/ @usecortex/hydradb-maintainers + +# API client +/src/hydradb_cli/client.py @usecortex/hydradb-maintainers + +# CI and tooling +/.github/ @usecortex/hydradb-maintainers diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..9cc704b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: Report a bug or unexpected behavior in HydraDB CLI +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thank you for reporting a bug. Please fill out the sections below to help us reproduce and fix the issue. + + - type: input + id: version + attributes: + label: CLI Version + description: "Output of `hydradb --version`" + placeholder: "hydradb-cli 0.1.0" + validations: + required: true + + - type: input + id: python-version + attributes: + label: Python Version + description: "Output of `python --version`" + placeholder: "Python 3.12.0" + validations: + required: true + + - type: input + id: os + attributes: + label: Operating System + placeholder: "macOS 14.2 / Ubuntu 22.04 / Windows 11" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear description of what happened and what you expected to happen. + placeholder: | + When I run `hydradb recall full "my query"`, I get an error... + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Minimal steps to reproduce the behavior. + placeholder: | + 1. Run `hydradb login` + 2. Run `hydradb recall full "test query"` + 3. See error + validations: + required: true + + - type: textarea + id: output + attributes: + label: CLI Output + description: Paste the full CLI output, including any error messages. + render: shell + validations: + required: false + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other information that might help (config, environment, workarounds tried). + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c77d6a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://docs.hydradb.com/ + about: Check the HydraDB documentation for usage guides and API reference. + - name: Security Vulnerability + url: https://github.com/usecortex/hydradb-cli/blob/main/SECURITY.md + about: Report security vulnerabilities privately (do not open a public issue). diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..5ade192 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: Feature Request +description: Suggest a new feature or improvement for HydraDB CLI +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thank you for suggesting a feature. Please describe what you'd like and why it would be useful. + + - type: textarea + id: problem + attributes: + label: Problem or Use Case + description: What problem does this feature solve? What are you trying to do? + placeholder: "I often need to... but currently I have to..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How would you like this to work? Include example commands if possible. + placeholder: | + hydradb recall full "my query" --top-k 5 --format json + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any workarounds or alternative approaches you've considered. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8cc7a3f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## What does this PR do? + + + +Fixes # + +## Type of change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Chore (CI, tooling, dependencies) + +## Checklist + +- [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide +- [ ] My code follows the project's style guidelines (`make lint` passes) +- [ ] I have added tests for my changes (`make test` passes) +- [ ] All new and existing tests pass +- [ ] My commits are signed off (`git commit -s`) +- [ ] I have updated documentation if needed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..049c7bb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install ruff + run: pip install "ruff>=0.4" + + - name: Run linting + run: ruff check . + + - name: Run format check + run: ruff format --check . + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package with test dependencies + run: pip install -e ".[dev]" + + - name: Run tests + run: pytest -q + + dco-check: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Fetch base branch for commit range + run: git fetch origin ${{ github.event.pull_request.base.ref }} + + - name: Check DCO sign-off + run: | + base_sha="${{ github.event.pull_request.base.sha }}" + failed=0 + for sha in $(git rev-list "$base_sha"..HEAD); do + if ! git log -1 --format='%B' "$sha" | grep -q "^Signed-off-by: "; then + echo "Commit $sha is missing a DCO sign-off:" + git log -1 --format=' %h %s' "$sha" + failed=1 + fi + done + if [ "$failed" -eq 1 ]; then + echo "" + echo "All commits must include a Signed-off-by line." + echo "" + echo "To fix the most recent commit:" + echo " git commit --amend -s" + echo "" + echo "To fix all commits in this PR:" + echo " git rebase --signoff $base_sha" + exit 1 + fi + echo "All commits have DCO sign-off." diff --git a/.gitignore b/.gitignore index a4dd5d9..cc7ca25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,35 @@ +# Environment +.env +.env.local +.env.*.local + +# Python +.venv/ +venv/ +env/ __pycache__/ *.py[cod] *$py.class *.egg-info/ +*.egg +.eggs/ dist/ build/ -.eggs/ -*.egg -.venv/ -venv/ -env/ -.env *.so -.mypy_cache/ + +# Tools .pytest_cache/ +.ruff_cache/ +.mypy_cache/ .coverage htmlcov/ + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..44d6f90 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,37 @@ +# Code of Conduct + +## Our Pledge + +We are committed to making participation in the HydraDB CLI project a welcoming and harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +**Positive behavior includes:** + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community and the project +- Showing empathy toward other community members + +**Unacceptable behavior includes:** + +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- The use of sexualized language or imagery and unwelcome sexual attention +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement + +Instances of unacceptable behavior may be reported by contacting the project maintainers at **conduct@hydradb.com**. All reports will be reviewed and investigated promptly and fairly. The project team is obligated to maintain confidentiality with regard to the reporter. + +Maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Scope + +This Code of Conduct applies within all project spaces -- including the repository, issue tracker, pull requests, discussions, and any other communication channels -- as well as in public spaces when an individual is representing the project or its community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3bb4302 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,237 @@ +# Contributing to HydraDB CLI + +Thank you for your interest in contributing to HydraDB CLI! This guide will help you get started. + +--- + +## Table of Contents + +- [Getting Started](#getting-started) +- [Branch Naming Convention](#branch-naming-convention) +- [Commit Message Format](#commit-message-format) +- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) +- [Code Style](#code-style) +- [Testing](#testing) +- [Pull Request Process](#pull-request-process) +- [Reporting Issues](#reporting-issues) + +--- + +## Getting Started + +### Prerequisites + +- Python 3.10 or later +- Git + +### Quick setup + +```bash +git clone https://github.com/usecortex/hydradb-cli.git +cd hydradb-cli +make bootstrap +source .venv/bin/activate +``` + +This creates a virtual environment, installs all dependencies (including dev tools like ruff and pytest), and makes the `hydradb` command available. + +### Manual setup + +If you prefer to do it manually: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +``` + +### Verify your setup + +```bash +hydradb --version # CLI works +make lint # linter passes +make test # all tests pass +``` + +--- + +## Branch Naming Convention + +Create a new branch from `main` for every change. Use the following prefixes: + +- `feat/` -- new features (e.g., `feat/batch-upload`) +- `fix/` -- bug fixes (e.g., `fix/timeout-handling`) +- `docs/` -- documentation changes (e.g., `docs/update-readme`) +- `chore/` -- maintenance, CI, and tooling (e.g., `chore/update-dependencies`) + +--- + +## Commit Message Format + +This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification. + +### Format + +``` +(): + +[optional body] + +[optional footer] +Signed-off-by: Your Name +``` + +### Types + +| Type | Description | +|------|-------------| +| `feat` | A new feature | +| `fix` | A bug fix | +| `docs` | Documentation only | +| `style` | Formatting, missing semicolons, etc. | +| `refactor` | Code change that neither fixes a bug nor adds a feature | +| `test` | Adding or correcting tests | +| `chore` | Maintenance tasks (CI, build, deps) | + +### Examples + +``` +feat(recall): add --top-k flag to full recall command + +fix(client): handle 429 rate-limit responses with retry-after header + +docs: update installation instructions for Python 3.13 +``` + +--- + +## Developer Certificate of Origin (DCO) + +All commits must be signed off to certify that you wrote or have the right to submit the code. Add a sign-off line to every commit: + +```bash +git commit -s -m "feat(recall): add --top-k flag" +``` + +This adds a `Signed-off-by: Your Name ` trailer. CI will reject commits without it. + +If you forget, amend the most recent commit: + +```bash +git commit --amend -s +``` + +Or sign off all commits in a PR: + +```bash +git rebase --signoff main +``` + +--- + +## Code Style + +Formatting and linting are enforced by [ruff](https://docs.astral.sh/ruff/). It is installed automatically with `make bootstrap` or `pip install -e ".[dev]"`. Run before committing: + +```bash +make lint # check for issues +make format # auto-fix formatting +``` + +### Key rules + +- **Line length:** 120 characters (enforced by ruff formatter) +- **Import sorting:** handled by ruff's isort rules +- **Type hints:** use them for all public function signatures +- **Docstrings:** required for all public functions and classes + +### Ruff configuration + +The full ruff configuration lives in `pyproject.toml` under `[tool.ruff]`. The selected rule sets are: + +| Rule | Description | +|------|-------------| +| E/W | pycodestyle errors and warnings | +| F | pyflakes | +| I | isort (import sorting) | +| N | pep8-naming | +| UP | pyupgrade | +| B | flake8-bugbear | +| S | flake8-bandit (security) | +| T20 | flake8-print | +| SIM | flake8-simplify | + +--- + +## Testing + +The test suite uses [pytest](https://docs.pytest.org/) with [typer.testing.CliRunner](https://typer.tiangolo.com/tutorial/testing/) for CLI integration tests. + +### Running tests + +```bash +make test # quick run +make coverage # with coverage report +pytest -v # verbose output +pytest tests/test_client.py -v # run a specific test file +``` + +### Writing tests + +- Place test files in `tests/` with the `test_` prefix +- Use `typer.testing.CliRunner` for CLI command tests +- Mock `HydraDBClient` methods to avoid real API calls +- Follow the existing patterns in `tests/test_cli_commands.py` + +### Test structure + +``` +tests/ +├── test_cli_commands.py # CLI integration tests (CliRunner) +├── test_client.py # HTTP client unit tests +├── test_config.py # Configuration management tests +└── test_output.py # Output formatting tests +``` + +--- + +## Pull Request Process + +1. **Create a branch** from `main` using the naming convention above +2. **Make your changes** in small, focused commits +3. **Run the checks** before pushing: + ```bash + make lint + make test + ``` +4. **Push and open a PR** against `main` +5. **Fill out the PR template** -- describe what changed and why +6. **Ensure CI passes** -- all status checks must be green +7. **Request review** -- a maintainer will review your PR + +### PR checklist + +- [ ] Code follows the project's style guidelines (`make lint` passes) +- [ ] Tests pass (`make test` passes) +- [ ] New code has corresponding tests +- [ ] All commits are signed off (DCO) +- [ ] PR description explains the change + +--- + +## Reporting Issues + +Use [GitHub Issues](https://github.com/usecortex/hydradb-cli/issues) to report bugs or request features. Please use the provided templates: + +- **Bug Report** -- for unexpected behavior or errors +- **Feature Request** -- for new functionality ideas + +Before opening a new issue, search existing issues to avoid duplicates. + +--- + +## Questions? + +If you have questions about contributing, open a [Discussion](https://github.com/usecortex/hydradb-cli/discussions) or reach out to the maintainers. + +Thank you for helping make HydraDB CLI better! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b828486 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +.DEFAULT_GOAL := help +VENV := .venv + +# ── Bootstrap ──────────────────────────────────────────────────────────────── + +.PHONY: bootstrap +bootstrap: ## Create venv and install package with dev tools + @./scripts/bootstrap.sh + +# ── Quality ────────────────────────────────────────────────────────────────── + +.PHONY: lint +lint: ## Run ruff linter and format check + @test -d "$(VENV)" || { echo "No venv found. Run 'make bootstrap' first."; exit 1; } + $(VENV)/bin/ruff check . + $(VENV)/bin/ruff format --check . + +.PHONY: format +format: ## Auto-fix lint issues and reformat code + @test -d "$(VENV)" || { echo "No venv found. Run 'make bootstrap' first."; exit 1; } + $(VENV)/bin/ruff check --fix . + $(VENV)/bin/ruff format . + +.PHONY: test +test: ## Run the test suite + @test -d "$(VENV)" || { echo "No venv found. Run 'make bootstrap' first."; exit 1; } + $(VENV)/bin/pytest -q + +.PHONY: coverage +coverage: ## Run tests with coverage report + @test -d "$(VENV)" || { echo "No venv found. Run 'make bootstrap' first."; exit 1; } + $(VENV)/bin/pytest --cov=hydradb_cli --cov-report=term-missing -q + +# ── Utilities ──────────────────────────────────────────────────────────────── + +.PHONY: clean +clean: ## Remove build artifacts and caches + rm -rf build/ dist/ *.egg-info src/*.egg-info + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type f -name '*.pyc' -delete 2>/dev/null || true + +.PHONY: help +help: ## Show this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..56b64c4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in HydraDB CLI, please report it responsibly. **Do not open a public GitHub issue.** + +Send an email to **security@hydradb.com** with the following information: + +1. A description of the vulnerability +2. Steps to reproduce the issue +3. The potential impact +4. Any suggested fixes (optional but appreciated) + +## Response Timeline + +- **Acknowledgment:** within 48 hours of your report +- **Initial assessment:** within 5 business days +- **Fix or mitigation:** as soon as reasonably possible, depending on severity + +We will coordinate with you on disclosure timing. We ask that you give us reasonable time to address the issue before making it public. + +## Scope + +This policy covers the HydraDB CLI tool and its source code. It does not cover the HydraDB API service itself -- for API security issues, contact the HydraDB team directly at https://hydradb.com. + +### In scope + +- Authentication and credential handling (`~/.hydradb/config.json`) +- API key exposure or leakage +- Command injection or path traversal +- Dependency vulnerabilities +- Insecure defaults + +### Out of scope + +- The HydraDB API service +- Third-party dependencies (report those to the upstream project) +- Social engineering attacks + +## Credential Security + +HydraDB CLI stores API keys in `~/.hydradb/config.json` with restrictive file permissions (0600). The CLI also supports environment variables (`HYDRA_DB_API_KEY`) as an alternative to file-based storage. API keys are masked in all CLI output. + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| 0.1.x | Yes | + +We recommend always using the latest release. diff --git a/pyproject.toml b/pyproject.toml index 1f01433..237ea53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,43 @@ Homepage = "https://hydradb.com/" Documentation = "https://docs.hydradb.com/" Repository = "https://github.com/usecortex/hydradb-cli" +[project.optional-dependencies] +dev = [ + "ruff>=0.4", + "pytest>=7.0", + "pytest-cov>=4.0", +] + [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "S", # flake8-bandit (security) + "T20", # flake8-print + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "S101", # assert usage (acceptable in tests and CLI) + "T201", # print statements (CLI tool uses print) + "B008", # function call in default argument (standard Typer pattern) +] + +[tool.ruff.lint.isort] +known-first-party = ["hydradb_cli"] diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..b3383d1 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Bootstrap a development environment for hydradb-cli. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +VENV_DIR="${REPO_ROOT}/.venv" + +# ── Python check ───────────────────────────────────────────────────────────── +PYTHON="" +for candidate in python3.13 python3.12 python3.11 python3.10 python3; do + if command -v "$candidate" &>/dev/null; then + PYTHON="$candidate" + break + fi +done + +if [ -z "$PYTHON" ]; then + echo "[!] Python 3.10+ is required but not found." + exit 1 +fi + +version=$($PYTHON -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') + +# Verify the detected Python is actually 3.10+ +if ! $PYTHON -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)'; then + echo "[!] Python ${version} detected but 3.10+ is required." + exit 1 +fi + +echo "[ok] Python ${version} detected" + +# ── Virtual environment ────────────────────────────────────────────────────── +if [ ! -d "$VENV_DIR" ]; then + echo "[ok] Creating virtual environment at ${VENV_DIR}" + $PYTHON -m venv "$VENV_DIR" +else + echo "[ok] Virtual environment already exists at ${VENV_DIR}" +fi + +# shellcheck disable=SC1091 +source "${VENV_DIR}/bin/activate" +echo "[ok] Activated virtual environment" + +# ── Install ────────────────────────────────────────────────────────────────── +pip install --quiet --upgrade pip +echo "[ok] pip upgraded" + +pip install --quiet -e "${REPO_ROOT}[dev]" +echo "[ok] Installed package in editable mode (with dev tools)" + +# ── Done ───────────────────────────────────────────────────────────────────── +echo "" +echo "[ok] Bootstrap complete! Next steps:" +echo " 1. Activate the venv: source .venv/bin/activate" +echo " 2. Run the CLI: hydradb --help" +echo " 3. Run tests: make test" +echo "" +echo " Run 'make help' to see all available targets." diff --git a/src/hydradb_cli/client.py b/src/hydradb_cli/client.py index 4cefabc..25e1fb9 100644 --- a/src/hydradb_cli/client.py +++ b/src/hydradb_cli/client.py @@ -7,7 +7,7 @@ import json import uuid from pathlib import Path -from typing import Any, Optional +from typing import Any import httpx @@ -28,8 +28,8 @@ class HydraDBClient: def __init__( self, - api_key: Optional[str] = None, - base_url: Optional[str] = None, + api_key: str | None = None, + base_url: str | None = None, timeout: float = 60.0, ): self.api_key = api_key or get_api_key() @@ -70,9 +70,7 @@ def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response: 0, f"Could not connect to {self.base_url}. Check your network and base URL." ) from e except httpx.ConnectTimeout as e: - raise HydraDBClientError( - 0, f"Connection timed out reaching {self.base_url}." - ) from e + raise HydraDBClientError(0, f"Connection timed out reaching {self.base_url}.") from e except httpx.ReadTimeout as e: raise HydraDBClientError( 0, "Request timed out waiting for a response. The server may be under heavy load." @@ -99,15 +97,16 @@ def _handle_response(self, response: httpx.Response) -> Any: def create_tenant( self, tenant_id: str, - is_embeddings_tenant: Optional[bool] = None, - embeddings_dimension: Optional[int] = None, + is_embeddings_tenant: bool | None = None, + embeddings_dimension: int | None = None, ) -> dict: body: dict[str, Any] = {"tenant_id": tenant_id} if is_embeddings_tenant is not None: body["is_embeddings_tenant"] = is_embeddings_tenant if embeddings_dimension is not None: body["embeddings_dimension"] = embeddings_dimension - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/tenants/create", headers=self._headers(), json=body, @@ -115,7 +114,8 @@ def create_tenant( return self._handle_response(resp) def monitor_tenant(self, tenant_id: str) -> dict: - resp = self._request("get", + resp = self._request( + "get", f"{self.base_url}/tenants/monitor", headers=self._headers(), params={"tenant_id": tenant_id}, @@ -123,7 +123,8 @@ def monitor_tenant(self, tenant_id: str) -> dict: return self._handle_response(resp) def list_sub_tenants(self, tenant_id: str) -> dict: - resp = self._request("get", + resp = self._request( + "get", f"{self.base_url}/tenants/sub_tenant_ids", headers=self._headers(), params={"tenant_id": tenant_id}, @@ -131,7 +132,8 @@ def list_sub_tenants(self, tenant_id: str) -> dict: return self._handle_response(resp) def delete_tenant(self, tenant_id: str) -> dict: - resp = self._request("delete", + resp = self._request( + "delete", f"{self.base_url}/tenants/delete", headers=self._headers(), params={"tenant_id": tenant_id}, @@ -144,12 +146,12 @@ def add_memory( self, tenant_id: str, text: str, - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, infer: bool = True, is_markdown: bool = False, - title: Optional[str] = None, - source_id: Optional[str] = None, - user_name: Optional[str] = None, + title: str | None = None, + source_id: str | None = None, + user_name: str | None = None, upsert: bool = True, ) -> dict: memory: dict[str, Any] = { @@ -172,7 +174,8 @@ def add_memory( if sub_tenant_id is not None: body["sub_tenant_id"] = sub_tenant_id - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/memories/add_memory", headers=self._headers(), json=body, @@ -182,7 +185,7 @@ def add_memory( def list_memories( self, tenant_id: str, - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, ) -> dict: body: dict[str, Any] = { "tenant_id": tenant_id, @@ -190,7 +193,8 @@ def list_memories( } if sub_tenant_id is not None: body["sub_tenant_id"] = sub_tenant_id - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/list/data", headers=self._headers(), json=body, @@ -201,7 +205,7 @@ def delete_memory( self, tenant_id: str, memory_id: str, - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, ) -> dict: params: dict[str, str] = { "tenant_id": tenant_id, @@ -209,7 +213,8 @@ def delete_memory( } if sub_tenant_id is not None: params["sub_tenant_id"] = sub_tenant_id - resp = self._request("delete", + resp = self._request( + "delete", f"{self.base_url}/memories/delete_memory", headers=self._headers(), params=params, @@ -222,9 +227,9 @@ def upload_knowledge( self, tenant_id: str, file_paths: list[str], - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, upsert: bool = False, - file_metadata: Optional[list[dict]] = None, + file_metadata: list[dict] | None = None, ) -> dict: paths = [] for fp in file_paths: @@ -253,7 +258,8 @@ def upload_knowledge( if file_metadata: data["file_metadata"] = json.dumps(file_metadata) - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/ingestion/upload_knowledge", headers=self._auth_headers(), data=data, @@ -268,9 +274,9 @@ def upload_text( self, tenant_id: str, text: str, - sub_tenant_id: Optional[str] = None, - title: Optional[str] = None, - source_id: Optional[str] = None, + sub_tenant_id: str | None = None, + title: str | None = None, + source_id: str | None = None, ) -> dict: """Upload text content as a knowledge source (via app_sources). @@ -294,7 +300,8 @@ def upload_text( data["sub_tenant_id"] = sub_tenant_id data["app_sources"] = json.dumps(source) - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/ingestion/upload_knowledge", headers=self._auth_headers(), data=data, @@ -305,7 +312,7 @@ def verify_processing( self, tenant_id: str, file_ids: list[str], - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, ) -> dict: params: dict[str, Any] = { "tenant_id": tenant_id, @@ -313,7 +320,8 @@ def verify_processing( } if sub_tenant_id is not None: params["sub_tenant_id"] = sub_tenant_id - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/ingestion/verify_processing", headers=self._headers(), params=params, @@ -324,7 +332,7 @@ def delete_knowledge( self, tenant_id: str, ids: list[str], - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, ) -> dict: body: dict[str, Any] = { "tenant_id": tenant_id, @@ -332,7 +340,8 @@ def delete_knowledge( } if sub_tenant_id is not None: body["sub_tenant_id"] = sub_tenant_id - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/knowledge/delete_knowledge", headers=self._headers(), json=body, @@ -346,13 +355,13 @@ def _recall( endpoint: str, tenant_id: str, query: str, - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, max_results: int = 10, - mode: Optional[str] = None, - alpha: Optional[float] = None, - recency_bias: Optional[float] = None, - graph_context: Optional[bool] = None, - additional_context: Optional[str] = None, + mode: str | None = None, + alpha: float | None = None, + recency_bias: float | None = None, + graph_context: bool | None = None, + additional_context: str | None = None, ) -> dict: """Shared recall logic for full_recall and recall_preferences.""" body: dict[str, Any] = { @@ -372,7 +381,8 @@ def _recall( body["graph_context"] = graph_context if additional_context: body["additional_context"] = additional_context - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/recall/{endpoint}", headers=self._headers(), json=body, @@ -389,10 +399,10 @@ def boolean_recall( self, tenant_id: str, query: str, - sub_tenant_id: Optional[str] = None, - operator: Optional[str] = None, + sub_tenant_id: str | None = None, + operator: str | None = None, max_results: int = 10, - search_mode: Optional[str] = None, + search_mode: str | None = None, ) -> dict: body: dict[str, Any] = { "tenant_id": tenant_id, @@ -405,7 +415,8 @@ def boolean_recall( body["operator"] = operator if search_mode: body["search_mode"] = search_mode - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/recall/boolean_recall", headers=self._headers(), json=body, @@ -417,10 +428,10 @@ def boolean_recall( def list_data( self, tenant_id: str, - sub_tenant_id: Optional[str] = None, - kind: Optional[str] = None, - page: Optional[int] = None, - page_size: Optional[int] = None, + sub_tenant_id: str | None = None, + kind: str | None = None, + page: int | None = None, + page_size: int | None = None, ) -> dict: body: dict[str, Any] = {"tenant_id": tenant_id} if sub_tenant_id is not None: @@ -431,7 +442,8 @@ def list_data( body["page"] = page if page_size is not None: body["page_size"] = page_size - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/list/data", headers=self._headers(), json=body, @@ -442,7 +454,7 @@ def fetch_content( self, tenant_id: str, source_id: str, - sub_tenant_id: Optional[str] = None, + sub_tenant_id: str | None = None, mode: str = "content", ) -> dict: body: dict[str, Any] = { @@ -452,7 +464,8 @@ def fetch_content( } if sub_tenant_id is not None: body["sub_tenant_id"] = sub_tenant_id - resp = self._request("post", + resp = self._request( + "post", f"{self.base_url}/fetch/content", headers=self._headers(), json=body, @@ -463,9 +476,9 @@ def graph_relations( self, tenant_id: str, source_id: str, - sub_tenant_id: Optional[str] = None, - is_memory: Optional[bool] = None, - limit: Optional[int] = None, + sub_tenant_id: str | None = None, + is_memory: bool | None = None, + limit: int | None = None, ) -> dict: params: dict[str, Any] = { "source_id": source_id, @@ -477,7 +490,8 @@ def graph_relations( params["is_memory"] = str(is_memory).lower() if limit is not None: params["limit"] = str(limit) - resp = self._request("get", + resp = self._request( + "get", f"{self.base_url}/list/graph_relations_by_id", headers=self._headers(), params=params, diff --git a/src/hydradb_cli/commands/auth.py b/src/hydradb_cli/commands/auth.py index 34c8882..ec90366 100644 --- a/src/hydradb_cli/commands/auth.py +++ b/src/hydradb_cli/commands/auth.py @@ -1,7 +1,6 @@ """Authentication commands: login, logout, whoami.""" import sys -from typing import Optional import typer from rich.panel import Panel @@ -19,29 +18,28 @@ print_error, print_json, print_result, - print_warning, spinner, ) from hydradb_cli.utils.common import mask_api_key def login( - api_key: Optional[str] = typer.Option( + api_key: str | None = typer.Option( None, "--api-key", help="Your HydraDB API key (Bearer token).", ), - tenant_id: Optional[str] = typer.Option( + tenant_id: str | None = typer.Option( None, "--tenant-id", help="Default tenant ID to use for all commands.", ), - sub_tenant_id: Optional[str] = typer.Option( + sub_tenant_id: str | None = typer.Option( None, "--sub-tenant-id", help="Default sub-tenant ID.", ), - base_url: Optional[str] = typer.Option( + base_url: str | None = typer.Option( None, "--base-url", help="Custom API base URL (default: https://api.hydradb.com).", @@ -63,7 +61,7 @@ def login( if not api_key or not api_key.strip(): print_error("API key cannot be empty.") - validation_warning: Optional[str] = None + validation_warning: str | None = None with HydraDBClient(api_key=api_key, base_url=base_url) as client: if tenant_id: @@ -100,7 +98,7 @@ def login( def fmt(r: dict): lines = [ "[green]\u2713[/green] Logged in to HydraDB", - f" [dim]Credentials saved to ~/.hydradb/config.json[/dim]", + " [dim]Credentials saved to ~/.hydradb/config.json[/dim]", ] if tenant_id: lines.append(f" [cyan]Tenant:[/cyan] {tenant_id}") diff --git a/src/hydradb_cli/commands/config_cmd.py b/src/hydradb_cli/commands/config_cmd.py index 6cf57b1..326dbab 100644 --- a/src/hydradb_cli/commands/config_cmd.py +++ b/src/hydradb_cli/commands/config_cmd.py @@ -1,7 +1,5 @@ """Configuration commands: show, set.""" -from typing import Optional - import typer from rich.panel import Panel @@ -101,5 +99,7 @@ def set_value( result = {"success": True, "key": key, "message": f"Set {key} in config."} print_result( result, - lambda r: f"[green]\u2713[/green] Set [cyan]{key}[/cyan] = [bold]{display_value}[/bold] in ~/.hydradb/config.json", + lambda r: ( + f"[green]\u2713[/green] Set [cyan]{key}[/cyan] = [bold]{display_value}[/bold] in ~/.hydradb/config.json" + ), ) diff --git a/src/hydradb_cli/commands/fetch.py b/src/hydradb_cli/commands/fetch.py index 5559952..d61245c 100644 --- a/src/hydradb_cli/commands/fetch.py +++ b/src/hydradb_cli/commands/fetch.py @@ -1,7 +1,5 @@ """Fetch commands: content, sources, relations.""" -from typing import Optional - import httpx import typer from rich.console import Group @@ -27,12 +25,8 @@ @app.command() def content( source_id: str = typer.Argument(help="Source ID to fetch content for."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), mode: str = typer.Option( "content", "--mode", @@ -95,7 +89,7 @@ def fmt(r: dict): return Panel( body, - title=f"[bold cyan]/// Source Content[/bold cyan]", + title="[bold cyan]/// Source Content[/bold cyan]", border_style="cyan", padding=(0, 1), ) @@ -115,23 +109,15 @@ def fmt(r: dict): @app.command() def sources( - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - kind: Optional[str] = typer.Option( + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + kind: str | None = typer.Option( None, "--kind", help="Filter by kind: 'knowledge' or 'memories'.", ), - page: Optional[int] = typer.Option( - None, "--page", help="Page number (1-indexed)." - ), - page_size: Optional[int] = typer.Option( - None, "--page-size", help="Items per page (1-100)." - ), + page: int | None = typer.Option(None, "--page", help="Page number (1-indexed)."), + page_size: int | None = typer.Option(None, "--page-size", help="Items per page (1-100)."), ) -> None: """List all ingested sources (knowledge and/or memories). @@ -180,7 +166,10 @@ def fmt(r: dict): rows.append([str(i), sid, src_title, stype]) table = make_table( - "#", "Source ID", "Title", "Type", + "#", + "Source ID", + "Title", + "Type", rows=rows, title=f"Found {len(sources_list)} source(s)", ) @@ -206,7 +195,9 @@ def fmt(r: dict): preview = mem_content[:100] + "..." if len(mem_content) > 100 else mem_content rows.append([str(i), mid, preview]) return make_table( - "#", "Memory ID", "Content", + "#", + "Memory ID", + "Content", rows=rows, title=f"Found {len(memories_list)} memory/memories", ) @@ -223,20 +214,14 @@ def fmt(r: dict): @app.command() def relations( source_id: str = typer.Argument(help="Source ID to fetch graph relations for."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - is_memory: Optional[bool] = typer.Option( + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + is_memory: bool | None = typer.Option( None, "--is-memory/--is-knowledge", help="Whether the source is a memory (vs knowledge).", ), - limit: Optional[int] = typer.Option( - None, "--limit", help="Maximum number of relations to return." - ), + limit: int | None = typer.Option(None, "--limit", help="Maximum number of relations to return."), ) -> None: """Fetch knowledge graph relations for a source. @@ -283,7 +268,9 @@ def fmt(r: dict): rows.append([src, pred, tgt]) return make_table( - "Subject", "Predicate", "Object", + "Subject", + "Predicate", + "Object", rows=rows, title=f"Graph relations for '{source_id}'", ) diff --git a/src/hydradb_cli/commands/knowledge.py b/src/hydradb_cli/commands/knowledge.py index 44db2d0..f3745c1 100644 --- a/src/hydradb_cli/commands/knowledge.py +++ b/src/hydradb_cli/commands/knowledge.py @@ -1,7 +1,5 @@ """Knowledge ingestion commands: upload, upload-text, verify, delete.""" -from typing import Optional - import httpx import typer from rich.panel import Panel @@ -37,7 +35,7 @@ } -def _human_status(raw: str, error_code: Optional[str] = None) -> str: +def _human_status(raw: str, error_code: str | None = None) -> str: """Map raw API status + error_code to a clear label.""" label = _STATUS_LABELS.get(raw.lower(), raw) if label == "errored" and error_code: @@ -57,15 +55,9 @@ def _status_style(label: str) -> str: @app.command() def upload( - files: list[str] = typer.Argument( - help="One or more file paths to upload (PDF, DOCX, TXT, etc.)." - ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + files: list[str] = typer.Argument(help="One or more file paths to upload (PDF, DOCX, TXT, etc.)."), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), upsert: bool = typer.Option( False, "--upsert", @@ -140,24 +132,16 @@ def fmt(r: dict): @app.command("upload-text") def upload_text( - text: Optional[str] = typer.Option( + text: str | None = typer.Option( None, "--text", "-t", help="Text content to upload as a knowledge source.", ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - title: Optional[str] = typer.Option( - None, "--title", help="Title for the knowledge source." - ), - source_id: Optional[str] = typer.Option( - None, "--source-id", help="Custom source ID." - ), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + title: str | None = typer.Option(None, "--title", help="Title for the knowledge source."), + source_id: str | None = typer.Option(None, "--source-id", help="Custom source ID."), ) -> None: """Upload text content to the knowledge base. @@ -191,7 +175,7 @@ def fmt(r: dict): preview = text[:80] + "..." if len(text) > 80 else text lines = [ f"[green]\u2713[/green] Knowledge source uploaded to tenant [bold]{tid}[/bold]", - f"[dim]\"{preview}\"[/dim]", + f'[dim]"{preview}"[/dim]', ] results = r.get("results", []) for item in results: @@ -208,15 +192,9 @@ def fmt(r: dict): @app.command() def verify( - file_ids: list[str] = typer.Argument( - help="One or more file/source IDs to check processing status." - ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + file_ids: list[str] = typer.Argument(help="One or more file/source IDs to check processing status."), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), ) -> None: """Check processing status of uploaded knowledge. @@ -282,15 +260,9 @@ def fmt(r: dict): @app.command() def delete( - ids: list[str] = typer.Argument( - help="One or more source IDs to delete from the knowledge base." - ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + ids: list[str] = typer.Argument(help="One or more source IDs to delete from the knowledge base."), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), confirm: bool = typer.Option( False, "--yes", @@ -333,7 +305,9 @@ def delete( ) print_result( result, - lambda r: f"[green]\u2713[/green] Deleted {len(clean_ids)} knowledge source(s) from tenant [bold]{tid}[/bold].", + lambda r: ( + f"[green]\u2713[/green] Deleted {len(clean_ids)} knowledge source(s) from tenant [bold]{tid}[/bold]." + ), ) except HydraDBClientError as e: handle_api_error(e) diff --git a/src/hydradb_cli/commands/memories.py b/src/hydradb_cli/commands/memories.py index ee6cd29..d2dab7e 100644 --- a/src/hydradb_cli/commands/memories.py +++ b/src/hydradb_cli/commands/memories.py @@ -1,7 +1,6 @@ """User memory commands: add, list, delete.""" import sys -from typing import Optional import httpx import typer @@ -23,18 +22,14 @@ @app.command() def add( - text: Optional[str] = typer.Option( + text: str | None = typer.Option( None, "--text", "-t", help="Text content to store as a memory. Use '-' to read from stdin.", ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), infer: bool = typer.Option( True, "--infer/--no-infer", @@ -45,15 +40,13 @@ def add( "--markdown", help="Treat the text as markdown content.", ), - title: Optional[str] = typer.Option( - None, "--title", help="Optional title for the memory." - ), - source_id: Optional[str] = typer.Option( + title: str | None = typer.Option(None, "--title", help="Optional title for the memory."), + source_id: str | None = typer.Option( None, "--source-id", help="Source identifier to group related memories.", ), - user_name: Optional[str] = typer.Option( + user_name: str | None = typer.Option( None, "--user-name", help="User name for personalization.", @@ -91,8 +84,7 @@ def add( text = stdin_data else: print_error( - "No text provided. Use --text 'your text', " - "pipe via stdin, or use --text - for interactive input." + "No text provided. Use --text 'your text', pipe via stdin, or use --text - for interactive input." ) if not text or not text.strip(): @@ -127,7 +119,7 @@ def fmt(r: dict): mark = "\u2713" if failed_count == 0 else "!" header = f"[{status}]{mark}[/{status}] Memory added ({success_count} success, {failed_count} failed)" - lines = [header, f"[dim]\"{preview}\"[/dim]"] + lines = [header, f'[dim]"{preview}"[/dim]'] results = r.get("results", []) for item in results: sid = item.get("source_id", "unknown") @@ -147,12 +139,8 @@ def fmt(r: dict): @app.command("list") def list_memories( - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), ) -> None: """List all user memories for a tenant. @@ -179,7 +167,9 @@ def fmt(r: dict): preview = content[:100] + "..." if len(content) > 100 else content rows.append([str(i), mid, preview]) return make_table( - "#", "Memory ID", "Content", + "#", + "Memory ID", + "Content", rows=rows, title=f"Found {len(memories)} memories", ) @@ -194,12 +184,8 @@ def fmt(r: dict): @app.command() def delete( memory_id: str = typer.Argument(help="ID of the memory to delete."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), confirm: bool = typer.Option( False, "--yes", diff --git a/src/hydradb_cli/commands/recall.py b/src/hydradb_cli/commands/recall.py index 7f6dac9..63eb2e1 100644 --- a/src/hydradb_cli/commands/recall.py +++ b/src/hydradb_cli/commands/recall.py @@ -1,7 +1,5 @@ """Recall commands: full, preferences, keyword.""" -from typing import Optional - import httpx import typer from rich.console import Group @@ -65,9 +63,9 @@ def _format_recall_result(r: dict): def _validate_recall_params( query: str, - mode: Optional[str], - alpha: Optional[float], - recency_bias: Optional[float], + mode: str | None, + alpha: float | None, + recency_bias: float | None, max_results: int, ) -> None: """Validate shared recall parameters before hitting the API.""" @@ -90,37 +88,31 @@ def _validate_recall_params( @app.command("full") def full_recall( query: str = typer.Argument(help="Search query to find relevant knowledge."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - max_results: int = typer.Option( - 10, "--max-results", "-n", help="Maximum number of results (1-50)." - ), - mode: Optional[str] = typer.Option( + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + max_results: int = typer.Option(10, "--max-results", "-n", help="Maximum number of results (1-50)."), + mode: str | None = typer.Option( None, "--mode", "-m", help="Retrieval mode: 'fast' or 'thinking' (deeper graph traversal).", ), - alpha: Optional[float] = typer.Option( + alpha: float | None = typer.Option( None, "--alpha", help="Hybrid search alpha (0.0=keyword, 1.0=semantic).", ), - recency_bias: Optional[float] = typer.Option( + recency_bias: float | None = typer.Option( None, "--recency-bias", help="Preference for newer content (0.0=none, 1.0=strong).", ), - graph_context: Optional[bool] = typer.Option( + graph_context: bool | None = typer.Option( None, "--graph-context/--no-graph-context", help="Include knowledge graph relations in results.", ), - additional_context: Optional[str] = typer.Option( + additional_context: str | None = typer.Option( None, "--context", help="Additional context to guide retrieval.", @@ -166,37 +158,31 @@ def full_recall( @app.command("preferences") def recall_preferences( query: str = typer.Argument(help="Search query to find relevant user memories."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - max_results: int = typer.Option( - 10, "--max-results", "-n", help="Maximum number of results (1-50)." - ), - mode: Optional[str] = typer.Option( + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + max_results: int = typer.Option(10, "--max-results", "-n", help="Maximum number of results (1-50)."), + mode: str | None = typer.Option( None, "--mode", "-m", help="Retrieval mode: 'fast' or 'thinking'.", ), - alpha: Optional[float] = typer.Option( + alpha: float | None = typer.Option( None, "--alpha", help="Hybrid search alpha (0.0=keyword, 1.0=semantic).", ), - recency_bias: Optional[float] = typer.Option( + recency_bias: float | None = typer.Option( None, "--recency-bias", help="Preference for newer content (0.0=none, 1.0=strong).", ), - graph_context: Optional[bool] = typer.Option( + graph_context: bool | None = typer.Option( None, "--graph-context/--no-graph-context", help="Include knowledge graph relations.", ), - additional_context: Optional[str] = typer.Option( + additional_context: str | None = typer.Option( None, "--context", help="Additional context to guide retrieval.", @@ -243,21 +229,15 @@ def recall_preferences( @app.command("keyword") def keyword_recall( query: str = typer.Argument(help="Keyword search terms."), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID. Uses default if not specified." - ), - sub_tenant_id: Optional[str] = typer.Option( - None, "--sub-tenant-id", help="Sub-tenant ID." - ), - operator: Optional[str] = typer.Option( + tenant_id: str | None = typer.Option(None, "--tenant-id", help="Tenant ID. Uses default if not specified."), + sub_tenant_id: str | None = typer.Option(None, "--sub-tenant-id", help="Sub-tenant ID."), + operator: str | None = typer.Option( None, "--operator", help="How to combine terms: 'or', 'and', or 'phrase'.", ), - max_results: int = typer.Option( - 10, "--max-results", "-n", help="Maximum number of results." - ), - search_mode: Optional[str] = typer.Option( + max_results: int = typer.Option(10, "--max-results", "-n", help="Maximum number of results."), + search_mode: str | None = typer.Option( None, "--search-mode", help="What to search: 'sources' (documents) or 'memories' (user memories).", diff --git a/src/hydradb_cli/commands/tenant.py b/src/hydradb_cli/commands/tenant.py index 5bb7624..f7d6afb 100644 --- a/src/hydradb_cli/commands/tenant.py +++ b/src/hydradb_cli/commands/tenant.py @@ -1,7 +1,5 @@ """Tenant management commands.""" -from typing import Optional - import httpx import typer from rich.panel import Panel @@ -21,7 +19,7 @@ def create( "--embeddings", help="Create as an embeddings tenant.", ), - embeddings_dimension: Optional[int] = typer.Option( + embeddings_dimension: int | None = typer.Option( None, "--embeddings-dimension", help="Embedding vector dimensions (required if --embeddings is set).", @@ -54,12 +52,15 @@ def create( @app.command() def monitor( - tenant_id_arg: Optional[str] = typer.Argument( - None, help="Tenant ID to monitor. Uses default if not specified.", + tenant_id_arg: str | None = typer.Argument( + None, + help="Tenant ID to monitor. Uses default if not specified.", metavar="TENANT_ID", ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID (alternative to positional argument).", + tenant_id: str | None = typer.Option( + None, + "--tenant-id", + help="Tenant ID (alternative to positional argument).", hidden=True, ), ) -> None: @@ -91,12 +92,15 @@ def fmt(r: dict): @app.command("list-sub-tenants") def list_sub_tenants( - tenant_id_arg: Optional[str] = typer.Argument( - None, help="Tenant ID. Uses default if not specified.", + tenant_id_arg: str | None = typer.Argument( + None, + help="Tenant ID. Uses default if not specified.", metavar="TENANT_ID", ), - tenant_id: Optional[str] = typer.Option( - None, "--tenant-id", help="Tenant ID (alternative to positional argument).", + tenant_id: str | None = typer.Option( + None, + "--tenant-id", + help="Tenant ID (alternative to positional argument).", hidden=True, ), ) -> None: diff --git a/src/hydradb_cli/config.py b/src/hydradb_cli/config.py index fe26b10..9979d05 100644 --- a/src/hydradb_cli/config.py +++ b/src/hydradb_cli/config.py @@ -8,7 +8,6 @@ import json import os from pathlib import Path -from typing import Optional # Environment variable names (aligned with MCP plugin and OpenClaw conventions) ENV_API_KEY = "HYDRA_DB_API_KEY" @@ -41,7 +40,7 @@ def _write_config_file(data: dict) -> None: CONFIG_FILE.chmod(0o600) -def get_api_key() -> Optional[str]: +def get_api_key() -> str | None: """Get API key from env var or config file.""" env_val = os.environ.get(ENV_API_KEY) if env_val: @@ -49,7 +48,7 @@ def get_api_key() -> Optional[str]: return _read_config_file().get("api_key") -def get_tenant_id() -> Optional[str]: +def get_tenant_id() -> str | None: """Get default tenant ID from env var or config file.""" env_val = os.environ.get(ENV_TENANT_ID) if env_val: @@ -57,7 +56,7 @@ def get_tenant_id() -> Optional[str]: return _read_config_file().get("tenant_id") -def get_sub_tenant_id() -> Optional[str]: +def get_sub_tenant_id() -> str | None: """Get default sub-tenant ID from env var or config file.""" env_val = os.environ.get(ENV_SUB_TENANT_ID) if env_val: @@ -74,10 +73,10 @@ def get_base_url() -> str: def save_config( - api_key: Optional[str] = None, - tenant_id: Optional[str] = None, - sub_tenant_id: Optional[str] = None, - base_url: Optional[str] = None, + api_key: str | None = None, + tenant_id: str | None = None, + sub_tenant_id: str | None = None, + base_url: str | None = None, ) -> None: """Save configuration values to config file.""" data = _read_config_file() @@ -108,5 +107,7 @@ def get_full_config() -> dict: "base_url": get_base_url(), "config_file": str(CONFIG_FILE), "api_key_source": "env" if os.environ.get(ENV_API_KEY) else ("file" if file_cfg.get("api_key") else "none"), - "tenant_id_source": "env" if os.environ.get(ENV_TENANT_ID) else ("file" if file_cfg.get("tenant_id") else "none"), + "tenant_id_source": "env" + if os.environ.get(ENV_TENANT_ID) + else ("file" if file_cfg.get("tenant_id") else "none"), } diff --git a/src/hydradb_cli/main.py b/src/hydradb_cli/main.py index 0adbd89..3a43934 100644 --- a/src/hydradb_cli/main.py +++ b/src/hydradb_cli/main.py @@ -16,8 +16,6 @@ config CLI configuration """ -from typing import Optional - import typer from hydradb_cli import __version__ @@ -38,9 +36,7 @@ def _version_callback(value: bool) -> None: if value: - console.print( - f"[bold cyan]///[/bold cyan] [bold]hydradb-cli[/bold] {__version__}" - ) + console.print(f"[bold cyan]///[/bold cyan] [bold]hydradb-cli[/bold] {__version__}") raise typer.Exit() @@ -53,7 +49,7 @@ def main( help="Output format: 'human' (default) or 'json'.", envvar="HYDRADB_OUTPUT", ), - version: Optional[bool] = typer.Option( + version: bool | None = typer.Option( None, "--version", "-v", diff --git a/src/hydradb_cli/output.py b/src/hydradb_cli/output.py index d91e0fd..671c427 100644 --- a/src/hydradb_cli/output.py +++ b/src/hydradb_cli/output.py @@ -6,26 +6,27 @@ """ import json +from collections.abc import Callable from contextlib import contextmanager -from typing import Any, Callable, Optional, Union +from typing import Any import typer from rich.console import Console, RenderableType -from rich.panel import Panel from rich.table import Table -from rich.text import Text from rich.theme import Theme -_THEME = Theme({ - "hydra.brand": "bold cyan", - "hydra.success": "green", - "hydra.error": "bold red", - "hydra.warning": "yellow", - "hydra.dim": "dim", - "hydra.key": "cyan", - "hydra.value": "white", - "hydra.accent": "bold cyan", -}) +_THEME = Theme( + { + "hydra.brand": "bold cyan", + "hydra.success": "green", + "hydra.error": "bold red", + "hydra.warning": "yellow", + "hydra.dim": "dim", + "hydra.key": "cyan", + "hydra.value": "white", + "hydra.accent": "bold cyan", + } +) console = Console(theme=_THEME, highlight=False) err_console = Console(stderr=True, theme=_THEME, highlight=False) @@ -82,7 +83,7 @@ def print_error(message: str, exit_code: int = 1) -> None: def print_result( data: Any, - human_formatter: Optional[Callable[[Any], Union[str, RenderableType]]] = None, + human_formatter: Callable[[Any], str | RenderableType] | None = None, ) -> None: """Print API result — JSON in json mode, formatted in human mode. @@ -92,11 +93,7 @@ def print_result( if _output_format == "json": print_json(data) elif human_formatter: - result = human_formatter(data) - if isinstance(result, str): - console.print(result) - else: - console.print(result) + console.print(human_formatter(data)) else: print_json(data) @@ -104,9 +101,7 @@ def print_result( def print_table(headers: list[str], rows: list[list[str]]) -> None: """Print a Rich table in human mode, or JSON array in json mode.""" if _output_format == "json": - items = [] - for row in rows: - items.append(dict(zip(headers, row))) + items = [dict(zip(headers, row, strict=True)) for row in rows] print_json(items) return @@ -131,7 +126,7 @@ def print_table(headers: list[str], rows: list[list[str]]) -> None: def make_table( *columns: str, rows: list[list[str]], - title: Optional[str] = None, + title: str | None = None, border_style: str = "dim", ) -> Table: """Build a Rich Table without printing it — callers compose into panels etc.""" @@ -151,7 +146,7 @@ def make_table( return table -def make_kv_table(pairs: list[tuple[str, str]], title: Optional[str] = None) -> Table: +def make_kv_table(pairs: list[tuple[str, str]], title: str | None = None) -> Table: """Build a two-column key-value table.""" table = Table( show_header=False, diff --git a/src/hydradb_cli/utils/common.py b/src/hydradb_cli/utils/common.py index de0d4d9..7ffb56a 100644 --- a/src/hydradb_cli/utils/common.py +++ b/src/hydradb_cli/utils/common.py @@ -1,13 +1,11 @@ """Common utilities shared across CLI commands.""" import sys -from typing import Optional import httpx -import typer from hydradb_cli.client import HydraDBClient, HydraDBClientError -from hydradb_cli.config import get_api_key, get_tenant_id, get_sub_tenant_id +from hydradb_cli.config import get_api_key, get_sub_tenant_id, get_tenant_id from hydradb_cli.output import print_error @@ -22,23 +20,19 @@ def require_api_key() -> str: """Get the API key or exit with a helpful error.""" key = get_api_key() if not key: - print_error( - "No API key configured. Run 'hydradb login' or set HYDRA_DB_API_KEY environment variable." - ) + print_error("No API key configured. Run 'hydradb login' or set HYDRA_DB_API_KEY environment variable.") return key # type: ignore[return-value] -def require_tenant_id(tenant_id: Optional[str] = None) -> str: +def require_tenant_id(tenant_id: str | None = None) -> str: """Get tenant ID from argument, config, or exit with error.""" tid = tenant_id or get_tenant_id() if not tid or not tid.strip(): - print_error( - "No tenant ID specified. Use --tenant-id or run 'hydradb config set tenant_id '." - ) + print_error("No tenant ID specified. Use --tenant-id or run 'hydradb config set tenant_id '.") return tid # type: ignore[return-value] -def resolve_sub_tenant_id(sub_tenant_id: Optional[str] = None) -> Optional[str]: +def resolve_sub_tenant_id(sub_tenant_id: str | None = None) -> str | None: """Get sub-tenant ID from argument or config (may be None).""" return sub_tenant_id or get_sub_tenant_id() @@ -52,6 +46,7 @@ def get_client() -> HydraDBClient: def _extract_error_message(detail: str) -> str: """Pull a human-readable message out of structured or raw error details.""" import ast + try: parsed = ast.literal_eval(detail) if isinstance(parsed, dict): @@ -81,8 +76,7 @@ def handle_api_error(e: HydraDBClientError) -> None: msg = _extract_error_message(e.detail) if "tenant collection statistics" in msg.lower(): print_error( - "Could not retrieve tenant stats. The tenant may not exist or " - "the backend is temporarily unavailable." + "Could not retrieve tenant stats. The tenant may not exist or the backend is temporarily unavailable." ) elif "memory service" in msg.lower(): print_error("Memory service is temporarily unavailable. Please try again.") @@ -95,10 +89,7 @@ def handle_api_error(e: HydraDBClientError) -> None: def handle_network_error(e: httpx.RequestError) -> None: """Format and print a network-level error, then exit.""" - print_error( - f"Network error: Unable to reach the HydraDB API. " - f"Check your connection and base URL. ({e})" - ) + print_error(f"Network error: Unable to reach the HydraDB API. Check your connection and base URL. ({e})") def validate_range(value: float, name: str, low: float, high: float) -> None: @@ -107,14 +98,14 @@ def validate_range(value: float, name: str, low: float, high: float) -> None: print_error(f"--{name} must be between {low} and {high}, got {value}") -def require_non_empty(value: Optional[str], name: str) -> str: +def require_non_empty(value: str | None, name: str) -> str: """Validate a string is non-empty/non-whitespace, or exit with error.""" if not value or not value.strip(): print_error(f"{name} cannot be empty.") return value.strip() # type: ignore[union-attr] -def read_stdin_safe() -> Optional[str]: +def read_stdin_safe() -> str | None: """Read from stdin if data is available, without hanging. Returns the stripped content or None if nothing is available. @@ -123,6 +114,7 @@ def read_stdin_safe() -> Optional[str]: return None import select + try: ready, _, _ = select.select([sys.stdin], [], [], 0.1) if ready: diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index 1af8fbf..e457dfc 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -86,10 +86,7 @@ def test_login_json_output(self, clean_config): mock_client.__enter__ = MagicMock(return_value=mock_client) mock_client.__exit__ = MagicMock(return_value=False) mock_cls.return_value = mock_client - result = runner.invoke(app, [ - "--output", "json", - "login", "--api-key", "test-key-abc", "--tenant-id", "t1" - ]) + result = runner.invoke(app, ["--output", "json", "login", "--api-key", "test-key-abc", "--tenant-id", "t1"]) assert result.exit_code == 0 data = json.loads(result.output) assert data["success"] is True @@ -101,14 +98,13 @@ def test_login_empty_key_fails(self, clean_config): def test_login_invalid_key_warns(self, clean_config): with patch("hydradb_cli.commands.auth.HydraDBClient") as mock_cls: from hydradb_cli.client import HydraDBClientError + mock_client = MagicMock() mock_client.monitor_tenant.side_effect = HydraDBClientError(403, "Forbidden") mock_client.__enter__ = MagicMock(return_value=mock_client) mock_client.__exit__ = MagicMock(return_value=False) mock_cls.return_value = mock_client - result = runner.invoke(app, [ - "login", "--api-key", "bad-key", "--tenant-id", "t1" - ]) + result = runner.invoke(app, ["login", "--api-key", "bad-key", "--tenant-id", "t1"]) assert result.exit_code == 0 assert "rejected" in result.output @@ -253,9 +249,7 @@ def test_memories_add(self, mock_get_client, clean_config): } mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "memories", "add", "--text", "User prefers dark mode" - ]) + result = runner.invoke(app, ["memories", "add", "--text", "User prefers dark mode"]) assert result.exit_code == 0 assert "Memory added" in result.output @@ -266,10 +260,7 @@ def test_memories_add_json(self, mock_get_client, clean_config): mock_client.add_memory.return_value = {"success_count": 1, "failed_count": 0} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "--output", "json", - "memories", "add", "--text", "test" - ]) + result = runner.invoke(app, ["--output", "json", "memories", "add", "--text", "test"]) assert result.exit_code == 0 data = json.loads(result.output) assert data["success_count"] == 1 @@ -347,9 +338,7 @@ def test_recall_full_json(self, mock_get_client, clean_config): mock_client.full_recall.return_value = {"chunks": []} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "--output", "json", "recall", "full", "test query" - ]) + result = runner.invoke(app, ["--output", "json", "recall", "full", "test query"]) assert result.exit_code == 0 data = json.loads(result.output) assert "chunks" in data @@ -371,9 +360,7 @@ def test_recall_keyword(self, mock_get_client, clean_config): mock_client.boolean_recall.return_value = {"chunks": []} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "recall", "keyword", "John Smith", "--operator", "phrase" - ]) + result = runner.invoke(app, ["recall", "keyword", "John Smith", "--operator", "phrase"]) assert result.exit_code == 0 @patch("hydradb_cli.commands.recall.get_client") @@ -410,14 +397,10 @@ def _setup_auth(self, clean_config): def test_knowledge_upload_text(self, mock_get_client, clean_config): self._setup_auth(clean_config) mock_client = MagicMock() - mock_client.upload_text.return_value = { - "results": [{"source_id": "src_1", "id": "src_1"}] - } + mock_client.upload_text.return_value = {"results": [{"source_id": "src_1", "id": "src_1"}]} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "knowledge", "upload-text", "--text", "Meeting notes content" - ]) + result = runner.invoke(app, ["knowledge", "upload-text", "--text", "Meeting notes content"]) assert result.exit_code == 0 assert "uploaded" in result.output.lower() @@ -426,18 +409,24 @@ def test_knowledge_upload_text_sub_tenant_wiring(self, mock_get_client, clean_co """--sub-tenant-id and --source-id flags are forwarded correctly to client.upload_text.""" self._setup_auth(clean_config) mock_client = MagicMock() - mock_client.upload_text.return_value = { - "results": [{"source_id": "my-sid", "id": "my-sid"}] - } + mock_client.upload_text.return_value = {"results": [{"source_id": "my-sid", "id": "my-sid"}]} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "knowledge", "upload-text", - "--text", "Q4 pricing: Starter $29, Pro $79", - "--sub-tenant-id", "sub-acme", - "--source-id", "my-sid", - "--title", "Pricing Notes", - ]) + result = runner.invoke( + app, + [ + "knowledge", + "upload-text", + "--text", + "Q4 pricing: Starter $29, Pro $79", + "--sub-tenant-id", + "sub-acme", + "--source-id", + "my-sid", + "--title", + "Pricing Notes", + ], + ) assert result.exit_code == 0 call_kwargs = mock_client.upload_text.call_args[1] assert call_kwargs["sub_tenant_id"] == "sub-acme" @@ -458,9 +447,7 @@ def test_knowledge_delete(self, mock_get_client, clean_config): mock_client.delete_knowledge.return_value = {"success": True} mock_get_client.return_value = mock_client - result = runner.invoke(app, [ - "knowledge", "delete", "doc1", "doc2", "--yes" - ]) + result = runner.invoke(app, ["knowledge", "delete", "doc1", "doc2", "--yes"]) assert result.exit_code == 0 @patch("hydradb_cli.commands.knowledge.get_client") @@ -522,6 +509,7 @@ def test_fetch_content(self, mock_get_client, clean_config): def test_fetch_content_not_found(self, mock_get_client, clean_config): self._setup_auth(clean_config) from hydradb_cli.client import HydraDBClientError + mock_client = MagicMock() mock_client.fetch_content.side_effect = HydraDBClientError(404, "File not found") mock_get_client.return_value = mock_client diff --git a/tests/test_client.py b/tests/test_client.py index 72aa252..31ed358 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,10 +18,12 @@ def client(): class TestClientInit: def test_default_base_url(self): - with patch("hydradb_cli.client.get_api_key", return_value="k"): - with patch("hydradb_cli.client.get_base_url", return_value="https://api.hydradb.com"): - c = HydraDBClient() - assert c.base_url == "https://api.hydradb.com" + with ( + patch("hydradb_cli.client.get_api_key", return_value="k"), + patch("hydradb_cli.client.get_base_url", return_value="https://api.hydradb.com"), + ): + c = HydraDBClient() + assert c.base_url == "https://api.hydradb.com" def test_custom_base_url(self): c = HydraDBClient(api_key="k", base_url="https://custom.com/") @@ -123,7 +125,7 @@ def test_list_memories(self, client): mock_resp.json.return_value = {"user_memories": []} mock_post.return_value = mock_resp - result = client.list_memories("t1") + client.list_memories("t1") body = mock_post.call_args[1]["json"] assert body["kind"] == "memories" @@ -134,7 +136,7 @@ def test_delete_memory(self, client): mock_resp.json.return_value = {"user_memory_deleted": True} mock_del.return_value = mock_resp - result = client.delete_memory("t1", "mem_123") + client.delete_memory("t1", "mem_123") params = mock_del.call_args[1]["params"] assert params["memory_id"] == "mem_123" @@ -244,9 +246,7 @@ def test_upload_text_with_sub_tenant(self, client): mock_resp.json.return_value = {"results": []} mock_post.return_value = mock_resp - client.upload_text( - tenant_id="t1", text="hello", sub_tenant_id="sub1" - ) + client.upload_text(tenant_id="t1", text="hello", sub_tenant_id="sub1") data = mock_post.call_args[1]["data"] app_sources = json.loads(data["app_sources"]) @@ -300,7 +300,7 @@ def test_fetch_content(self, client): mock_resp.json.return_value = {"content": "hello"} mock_post.return_value = mock_resp - result = client.fetch_content("t1", "src_1", mode="content") + client.fetch_content("t1", "src_1", mode="content") body = mock_post.call_args[1]["json"] assert body["source_id"] == "src_1" assert body["mode"] == "content" diff --git a/tests/test_config.py b/tests/test_config.py index 10d9860..217825b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,14 +1,8 @@ """Tests for hydradb_cli.config module.""" -import json -import os -from pathlib import Path -from unittest.mock import patch - import pytest from hydradb_cli.config import ( - CONFIG_FILE, DEFAULT_BASE_URL, ENV_API_KEY, ENV_BASE_URL,