Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ ocl repo export download CIEL CIEL v2026-03-23 --type source -o CIEL_v2026-03-23
```bash
# Search & browse
ocl concept search [QUERY] [--owner OWNER] [--repo REPO] [--concept-class CLASS]
ocl concept list [--owner OWNER] [--repo REPO] [--repo-type source|collection]
ocl concept get OWNER SOURCE CONCEPT_ID [--repo-version VERSION] [--include-mappings] [--include-inverse-mappings]
ocl concept versions OWNER SOURCE CONCEPT_ID
ocl concept names OWNER SOURCE CONCEPT_ID [--verbose]
Expand Down
11 changes: 6 additions & 5 deletions docs/demos/01-content-exploration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ Multi-step CLI scenarios that exercise real-world workflows against the producti
**Steps:**
1. `ocl org get CIEL` — org profile
2. `ocl org repos CIEL` — sources and collections owned by CIEL
3. `ocl concept search malaria --owner CIEL --repo CIEL` — find a concept
4. `ocl concept names CIEL CIEL 116128` — all translations (11 names across 9 languages)
5. `ocl mapping search --owner CIEL --repo CIEL --from-concept 138041 --verbose` — outbound mappings with target source display
6. `ocl mapping get CIEL CIEL 162820` — full detail showing from/to source URLs and codes
3. `ocl concept list --owner CIEL --repo CIEL --limit 3` — browse concepts without needing to know a search term
4. `ocl concept search malaria --owner CIEL --repo CIEL` — find a concept
5. `ocl concept names CIEL CIEL 116128` — all translations (11 names across 9 languages)
6. `ocl mapping search --owner CIEL --repo CIEL --from-concept 138041 --verbose` — outbound mappings with target source display
7. `ocl mapping get CIEL CIEL 162820` — full detail showing from/to source URLs and codes

**Validates:** org lookup, org repo listing, concept search, name listing, mapping search with target source display, mapping detail.
**Validates:** org lookup, org repo listing, concept browsing, concept search, name listing, mapping search with target source display, mapping detail.

---

Expand Down
1 change: 1 addition & 0 deletions docs/demos/07-agent-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Verify the CLI works well as a backend for AI agents and scripts.
## 7.1 JSON Output for Programmatic Consumption
```bash
# Every command should produce valid, parseable JSON with -j
ocl -j concept list --owner CIEL --repo CIEL --limit 2 | python -m json.tool
ocl -j concept search malaria --owner CIEL --repo CIEL --limit 2 | python -m json.tool
ocl -j concept get CIEL CIEL 116128 --include-mappings | python -m json.tool
ocl -j cascade Regenstrief LOINC 2345-7 --levels 1 --cascade-hierarchy --no-cascade-mappings --reverse | python -m json.tool
Expand Down
50 changes: 47 additions & 3 deletions src/ocl_cli/commands/concept.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
from ocl_cli.api_client import APIError
from ocl_cli.main import handle_api_error
from ocl_cli.output import (
output_result, format_concept_list, format_concept_detail,
format_version_list, format_names_list, format_descriptions_list,
format_extras, format_match_results,
format_concept_detail,
format_concept_list,
format_descriptions_list,
format_extras,
format_match_results,
format_names_list,
format_version_list,
output_result,
)


Expand Down Expand Up @@ -46,6 +51,45 @@ def search(ctx, query, owner, owner_type, repo, repo_type, repo_version, concept
datatype, locale, include_retired, include_mappings, include_inverse_mappings,
updated_since, sort, verbose, limit, page):
"""Search for concepts globally or within a repository."""
_search_concepts(
ctx, query, owner, owner_type, repo, repo_type, repo_version, concept_class,
datatype, locale, include_retired, include_mappings, include_inverse_mappings,
updated_since, sort, verbose, limit, page,
)


@concept.command("list")
@click.option("--owner", help="Filter by owner")
@click.option("--owner-type", type=click.Choice(["users", "orgs"]))
@click.option("--repo", help="Filter by source/collection")
@click.option("--repo-type", type=click.Choice(["source", "collection"]), default="source")
@click.option("--repo-version", help="Repository version")
@click.option("--concept-class", help="Filter by concept class")
@click.option("--datatype", help="Filter by datatype")
@click.option("--locale", help="Filter by locale")
@click.option("--include-retired", is_flag=True, help="Include retired concepts")
@click.option("--include-mappings", is_flag=True, help="Include mappings in results")
@click.option("--include-inverse-mappings", is_flag=True)
@click.option("--updated-since", help="Filter by update date (YYYY-MM-DD)")
@click.option("--sort", help="Sort field (prefix with - for descending)")
@click.option("--verbose", is_flag=True, help="Include extra detail")
@click.option("--limit", default=20, help="Results per page")
@click.option("--page", default=1, help="Page number")
@click.pass_context
def list_(ctx, owner, owner_type, repo, repo_type, repo_version, concept_class,
datatype, locale, include_retired, include_mappings, include_inverse_mappings,
updated_since, sort, verbose, limit, page):
"""List concepts globally or within a repository."""
_search_concepts(
ctx, None, owner, owner_type, repo, repo_type, repo_version, concept_class,
datatype, locale, include_retired, include_mappings, include_inverse_mappings,
updated_since, sort, verbose, limit, page,
)


def _search_concepts(ctx, query, owner, owner_type, repo, repo_type, repo_version, concept_class,
datatype, locale, include_retired, include_mappings, include_inverse_mappings,
updated_since, sort, verbose, limit, page):
client = ctx.obj["client"]
try:
result = client.search_concepts(
Expand Down
89 changes: 89 additions & 0 deletions tests/test_concept_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import unittest
from types import SimpleNamespace
from unittest.mock import patch

from click.testing import CliRunner

from ocl_cli.main import cli


class FakeClient:
def __init__(self):
self.search_concepts_calls = []
self.closed = False

def search_concepts(self, **kwargs):
self.search_concepts_calls.append(kwargs)
return {"count": 0, "results": []}

def close(self):
self.closed = True


class FakeConfig:
def get_server(self, server_id):
return SimpleNamespace(base_url="https://api.example.test")

def resolve_token(self, server, token_override=None):
return token_override


class ConceptCommandTest(unittest.TestCase):
def test_concept_list_delegates_to_search_without_query(self):
client = FakeClient()
runner = CliRunner()

with (
patch("ocl_cli.main.CLIConfig.load", return_value=FakeConfig()),
patch("ocl_cli.main.OCLAPIClient", return_value=client),
):
result = runner.invoke(
cli,
[
"concept",
"list",
"--owner",
"openmrs",
"--repo",
"BasicLabTests",
"--repo-type",
"collection",
"--limit",
"10",
"--page",
"2",
"--verbose",
],
)

self.assertEqual(result.exit_code, 0, result.output)
self.assertEqual(
client.search_concepts_calls,
[
{
"query": None,
"owner": "openmrs",
"owner_type": None,
"repo": "BasicLabTests",
"repo_type": "collection",
"repo_version": None,
"concept_class": None,
"datatype": None,
"locale": None,
"include_retired": False,
"include_mappings": False,
"include_inverse_mappings": False,
"updated_since": None,
"sort": None,
"verbose": True,
"limit": 10,
"page": 2,
}
],
)
self.assertEqual(result.output.strip(), "No concepts found.")
self.assertTrue(client.closed)


if __name__ == "__main__":
unittest.main()