irides-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: irides-cli
3
+ Version: 0.1.0
4
+ Summary: A command-line interface for discovering, inspecting, and managing structured database context for AI systems.
5
+ Author-email: Gian Andrea Sechi <me@gianandreasechi.com>
6
+ License: Apache-2.0
7
+ Project-URL: Source Code, https://github.com/GianAndreaSechi/irides/cli
8
+ Project-URL: Homepage, https://www.gianandreasechi.com
9
+ Keywords: database,introspection,cli,irides,schema
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: irides-core>=0.1.0
24
+
25
+ # Iride CLI
26
+
27
+ Command-line interface for database introspection, built only on the `core` package and Python's standard library. It does not require FastAPI, MCP, or the Redis worker.
28
+
29
+ ## Structure
30
+
31
+ The CLI is organized by responsibility:
32
+
33
+ - `irides_cli/presentation/`: command definitions and `argparse` parsing;
34
+ - `irides_cli/controllers/`: maps command-line arguments to use cases;
35
+ - `irides_cli/dto/`: immutable, typed request DTOs;
36
+ - `irides_cli/services/`: live introspection and metadata operations using only `core`;
37
+ - `irides_cli/main.py`: composition root, JSON serialization, and process-level error handling.
38
+
39
+ ## Local installation
40
+
41
+ ```bash
42
+ pip install -e ./core -e ./cli
43
+ ```
44
+
45
+ ## Quickstart & Configuration
46
+
47
+ Irides supports two ways to configure database targets:
48
+
49
+ ### Option 1: Declarative Config File (`irides.yaml`) — Recommended
50
+ Generate a template configuration with:
51
+ ```bash
52
+ irides init
53
+ ```
54
+ This creates an `irides.yaml` file in the current directory:
55
+ ```yaml
56
+ targets:
57
+ my_postgres:
58
+ type: postgres
59
+ host: localhost
60
+ port: 5432
61
+ user: postgres
62
+ password: "${PG_PASSWORD:-secret}"
63
+ database: my_database
64
+
65
+ local_sqlite:
66
+ type: sqlite
67
+ database: ./app.db
68
+ ```
69
+ You can pass a custom config file anytime using `-c` / `--config-file`:
70
+ ```bash
71
+ irides -c /path/to/my_config.yaml configurations
72
+ ```
73
+
74
+ ### Option 2: Environment Variables (`.env`)
75
+ You can initialize an environment template with:
76
+ ```bash
77
+ irides init --format env
78
+ ```
79
+ Or specify an explicit `.env` file via `-e` / `--env-file`:
80
+ ```bash
81
+ irides -e /path/to/.env configurations
82
+ ```
83
+
84
+ ## Commands
85
+
86
+ ```bash
87
+ # Initialize template configuration
88
+ irides init
89
+
90
+ # List configured database targets
91
+ irides configurations
92
+
93
+ # Test database connection
94
+ irides connect my_postgres
95
+
96
+ # List database instances
97
+ irides instances --target my_postgres
98
+
99
+ # List schemas in a database
100
+ irides schemas --target my_postgres --instance localhost
101
+
102
+ # List tables (supports --limit and --offset)
103
+ irides tables --target my_postgres --instance localhost --schema public --limit 50
104
+
105
+ # Introspect table schema
106
+ irides describe --target my_postgres --instance localhost --schema public --table orders
107
+ irides describe --target my_postgres --instance localhost --schema public --table orders --generate-ai-docs
108
+ irides describe --target my_postgres --instance localhost --schema public --table orders --no-export-okf
109
+ ```
110
+
111
+ > **Note**: `--target` and `--config` are interchangeable aliases.
112
+ Omitting `--target`, `--instance`, `--schema`, or `--table` expands the scope, just like the corresponding API endpoints. Results are always written as JSON to stdout; errors are written to stderr. Add `--no-cache` to introspection commands to bypass Redis.
113
+
114
+ `describe` saves canonical JSON metadata and generates both **Markdown** and **Open Knowledge Format (OKF v0.2)** exports by default.
115
+
116
+ Available options for `describe`:
117
+ - `--generate-ai-docs`: generate domain summary and column descriptions via LiteLLM.
118
+ - `--no-save-metadata`: skip saving the canonical JSON metadata file (exports are still generated if enabled).
119
+ - `--only-if-changed`: skip writing if the schema is identical to the stored version.
120
+ - `--no-export-markdown`: disable the default Markdown export.
121
+ - `--no-export-okf`: disable the default OKF catalog bundle generation.
122
+ - `--no-preformat`: export full metadata instead of the essential deterministic record.
123
+ - `--save-markdown`: legacy compatibility flag for Markdown export.
124
+
125
+ Artifacts are persisted under `STORAGE_EXPORT_DIR` (default `storage/exports`):
126
+ - `storage/exports/markdown/{config}/{instance}/{schema}/{table}.md`
127
+ - `storage/exports/okf/catalog/{config}/{instance}/{schema}/{table}.md` (along with `storage/exports/okf/catalog/index.md`)
128
+
129
+ ## Metadata
130
+
131
+ ```bash
132
+ irides metadata instances
133
+ irides metadata databases db1.company.com
134
+ irides metadata tables db1.company.com production
135
+ irides metadata get db1.company.com production orders
136
+ irides metadata update db1.company.com production orders '{"owner":"data-team","tags":["billing"]}'
137
+ ```
138
+
139
+ The CLI does not include `scan` commands. They are asynchronous and explicitly require Redis Streams and the `worker` service.
140
+
141
+ ## Docker
142
+
143
+ The CLI reads its configuration from `cli/.env` and uses the shared Redis network. Start by copying `.env.example` as shown above and configure at least one `DB_TARGETS` entry.
144
+
145
+ ```bash
146
+ docker compose -f infra/docker-compose.infra.yml up -d
147
+ docker compose -f cli/docker-compose.yml run --rm irides-cli configurations
148
+ docker compose -f cli/docker-compose.yml run --rm irides-cli tables --config sales_mysql --schema public
149
+ docker compose -f cli/docker-compose.yml run --rm irides-cli describe --config sales_mysql --schema public --table orders
150
+ ```
@@ -0,0 +1,126 @@
1
+ # Iride CLI
2
+
3
+ Command-line interface for database introspection, built only on the `core` package and Python's standard library. It does not require FastAPI, MCP, or the Redis worker.
4
+
5
+ ## Structure
6
+
7
+ The CLI is organized by responsibility:
8
+
9
+ - `irides_cli/presentation/`: command definitions and `argparse` parsing;
10
+ - `irides_cli/controllers/`: maps command-line arguments to use cases;
11
+ - `irides_cli/dto/`: immutable, typed request DTOs;
12
+ - `irides_cli/services/`: live introspection and metadata operations using only `core`;
13
+ - `irides_cli/main.py`: composition root, JSON serialization, and process-level error handling.
14
+
15
+ ## Local installation
16
+
17
+ ```bash
18
+ pip install -e ./core -e ./cli
19
+ ```
20
+
21
+ ## Quickstart & Configuration
22
+
23
+ Irides supports two ways to configure database targets:
24
+
25
+ ### Option 1: Declarative Config File (`irides.yaml`) — Recommended
26
+ Generate a template configuration with:
27
+ ```bash
28
+ irides init
29
+ ```
30
+ This creates an `irides.yaml` file in the current directory:
31
+ ```yaml
32
+ targets:
33
+ my_postgres:
34
+ type: postgres
35
+ host: localhost
36
+ port: 5432
37
+ user: postgres
38
+ password: "${PG_PASSWORD:-secret}"
39
+ database: my_database
40
+
41
+ local_sqlite:
42
+ type: sqlite
43
+ database: ./app.db
44
+ ```
45
+ You can pass a custom config file anytime using `-c` / `--config-file`:
46
+ ```bash
47
+ irides -c /path/to/my_config.yaml configurations
48
+ ```
49
+
50
+ ### Option 2: Environment Variables (`.env`)
51
+ You can initialize an environment template with:
52
+ ```bash
53
+ irides init --format env
54
+ ```
55
+ Or specify an explicit `.env` file via `-e` / `--env-file`:
56
+ ```bash
57
+ irides -e /path/to/.env configurations
58
+ ```
59
+
60
+ ## Commands
61
+
62
+ ```bash
63
+ # Initialize template configuration
64
+ irides init
65
+
66
+ # List configured database targets
67
+ irides configurations
68
+
69
+ # Test database connection
70
+ irides connect my_postgres
71
+
72
+ # List database instances
73
+ irides instances --target my_postgres
74
+
75
+ # List schemas in a database
76
+ irides schemas --target my_postgres --instance localhost
77
+
78
+ # List tables (supports --limit and --offset)
79
+ irides tables --target my_postgres --instance localhost --schema public --limit 50
80
+
81
+ # Introspect table schema
82
+ irides describe --target my_postgres --instance localhost --schema public --table orders
83
+ irides describe --target my_postgres --instance localhost --schema public --table orders --generate-ai-docs
84
+ irides describe --target my_postgres --instance localhost --schema public --table orders --no-export-okf
85
+ ```
86
+
87
+ > **Note**: `--target` and `--config` are interchangeable aliases.
88
+ Omitting `--target`, `--instance`, `--schema`, or `--table` expands the scope, just like the corresponding API endpoints. Results are always written as JSON to stdout; errors are written to stderr. Add `--no-cache` to introspection commands to bypass Redis.
89
+
90
+ `describe` saves canonical JSON metadata and generates both **Markdown** and **Open Knowledge Format (OKF v0.2)** exports by default.
91
+
92
+ Available options for `describe`:
93
+ - `--generate-ai-docs`: generate domain summary and column descriptions via LiteLLM.
94
+ - `--no-save-metadata`: skip saving the canonical JSON metadata file (exports are still generated if enabled).
95
+ - `--only-if-changed`: skip writing if the schema is identical to the stored version.
96
+ - `--no-export-markdown`: disable the default Markdown export.
97
+ - `--no-export-okf`: disable the default OKF catalog bundle generation.
98
+ - `--no-preformat`: export full metadata instead of the essential deterministic record.
99
+ - `--save-markdown`: legacy compatibility flag for Markdown export.
100
+
101
+ Artifacts are persisted under `STORAGE_EXPORT_DIR` (default `storage/exports`):
102
+ - `storage/exports/markdown/{config}/{instance}/{schema}/{table}.md`
103
+ - `storage/exports/okf/catalog/{config}/{instance}/{schema}/{table}.md` (along with `storage/exports/okf/catalog/index.md`)
104
+
105
+ ## Metadata
106
+
107
+ ```bash
108
+ irides metadata instances
109
+ irides metadata databases db1.company.com
110
+ irides metadata tables db1.company.com production
111
+ irides metadata get db1.company.com production orders
112
+ irides metadata update db1.company.com production orders '{"owner":"data-team","tags":["billing"]}'
113
+ ```
114
+
115
+ The CLI does not include `scan` commands. They are asynchronous and explicitly require Redis Streams and the `worker` service.
116
+
117
+ ## Docker
118
+
119
+ The CLI reads its configuration from `cli/.env` and uses the shared Redis network. Start by copying `.env.example` as shown above and configure at least one `DB_TARGETS` entry.
120
+
121
+ ```bash
122
+ docker compose -f infra/docker-compose.infra.yml up -d
123
+ docker compose -f cli/docker-compose.yml run --rm irides-cli configurations
124
+ docker compose -f cli/docker-compose.yml run --rm irides-cli tables --config sales_mysql --schema public
125
+ docker compose -f cli/docker-compose.yml run --rm irides-cli describe --config sales_mysql --schema public --table orders
126
+ ```
@@ -0,0 +1,2 @@
1
+ """Command-line interface for Iride's core introspection library."""
2
+
@@ -0,0 +1,2 @@
1
+ """Adapters between the command-line presentation layer and services."""
2
+
@@ -0,0 +1,78 @@
1
+ """Controller for initializing configuration templates."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+
8
+ SAMPLE_YAML = """# irides.yaml - Irides database configuration file
9
+ # Learn more: https://github.com/GianAndreaSechi/irides
10
+
11
+ targets:
12
+ # SQLite example (no server needed)
13
+ # local_sqlite:
14
+ # type: sqlite
15
+ # database: ./data.db
16
+
17
+ # PostgreSQL example
18
+ # my_postgres:
19
+ # type: postgres
20
+ # host: localhost
21
+ # port: 5432
22
+ # user: postgres
23
+ # password: "${PG_PASSWORD:-postgres}"
24
+ # database: my_database
25
+
26
+ # MySQL example
27
+ # my_mysql:
28
+ # type: mysql
29
+ # host: localhost
30
+ # port: 3306
31
+ # user: root
32
+ # password: "${MYSQL_PASSWORD}"
33
+
34
+ # MongoDB example
35
+ # my_mongo:
36
+ # type: mongodb
37
+ # host: localhost
38
+ # port: 27017
39
+ # username: admin
40
+ # password: "${MONGO_PASSWORD}"
41
+ # authSource: admin
42
+ """
43
+
44
+ SAMPLE_ENV = """# .env - Irides environment configuration
45
+ # Learn more: https://github.com/GianAndreaSechi/irides
46
+
47
+ DB_TARGETS=sample_pg
48
+
49
+ # PostgreSQL sample target
50
+ DB_TARGET_SAMPLE_PG_TYPE=postgres
51
+ DB_TARGET_SAMPLE_PG_HOST=localhost
52
+ DB_TARGET_SAMPLE_PG_PORT=5432
53
+ DB_TARGET_SAMPLE_PG_USER=postgres
54
+ DB_TARGET_SAMPLE_PG_PASSWORD=secret
55
+ DB_TARGET_SAMPLE_PG_DATABASE=my_database
56
+ """
57
+
58
+
59
+ class InitController:
60
+ """Handles the 'init' CLI command."""
61
+
62
+ def execute(self, fmt: str = "yaml", force: bool = False) -> Dict[str, Any]:
63
+ target_filename = "irides.yaml" if fmt == "yaml" else ".env"
64
+ target_path = Path(target_filename)
65
+
66
+ if target_path.exists() and not force:
67
+ raise ValueError(
68
+ f"'{target_filename}' already exists. Use --force to overwrite."
69
+ )
70
+
71
+ content = SAMPLE_YAML if fmt == "yaml" else SAMPLE_ENV
72
+ target_path.write_text(content, encoding="utf-8")
73
+
74
+ return {
75
+ "status": "created",
76
+ "file": target_filename,
77
+ "message": f"Template created at '{target_filename}'. Edit your database credentials and run 'irides configurations'.",
78
+ }
@@ -0,0 +1,34 @@
1
+ """Controller that maps parsed CLI arguments into introspection DTOs."""
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from irides_cli.dto.requests import DescribeRequest, ScopeRequest, TablesRequest
7
+ from irides_cli.services.introspection_service import IntrospectionService
8
+
9
+
10
+ class IntrospectionController:
11
+ def __init__(self, service: IntrospectionService | None = None, config_file: str | None = None) -> None:
12
+ self.service = service or IntrospectionService(config_file=config_file)
13
+
14
+ def execute(self, args: argparse.Namespace) -> Any:
15
+ if args.command == "configurations": return self.service.configurations()
16
+ if args.command == "connect": return self.service.connect(args.config_name)
17
+ if args.command == "instances": return self.service.instances(ScopeRequest(config_name=args.config_name, no_cache=args.no_cache))
18
+ if args.command == "schemas": return self.service.schemas(ScopeRequest(args.config_name, args.instance_name, args.no_cache))
19
+ if args.command == "tables": return self.service.tables(TablesRequest(args.config_name, args.instance_name, args.no_cache, args.schema_name, args.limit, args.offset))
20
+ return self.service.describe(
21
+ DescribeRequest(
22
+ config_name=args.config_name,
23
+ instance_name=args.instance_name,
24
+ no_cache=args.no_cache,
25
+ schema_name=args.schema_name,
26
+ table_name=args.table_name,
27
+ generate_ai_docs=args.generate_ai_docs,
28
+ save_metadata=args.save_metadata,
29
+ only_if_changed=args.only_if_changed,
30
+ export_markdown=args.export_markdown,
31
+ export_okf=args.export_okf,
32
+ preformat=args.preformat,
33
+ )
34
+ )
@@ -0,0 +1,23 @@
1
+ """Controller for metadata commands and payload validation."""
2
+
3
+ import argparse
4
+ import json
5
+ from typing import Any
6
+
7
+ from irides_cli.dto.requests import MetadataUpdateRequest, PageRequest
8
+ from irides_cli.services.metadata_service import MetadataService
9
+
10
+
11
+ class MetadataController:
12
+ def __init__(self, service: MetadataService | None = None) -> None:
13
+ self.service = service or MetadataService()
14
+
15
+ def execute(self, args: argparse.Namespace) -> Any:
16
+ page = PageRequest(args.page, args.page_size) if hasattr(args, "page") else None
17
+ if args.metadata_command == "instances": return self.service.instances(page)
18
+ if args.metadata_command == "databases": return self.service.databases(args.instance, page)
19
+ if args.metadata_command == "tables": return self.service.tables(args.instance, args.database, page)
20
+ if args.metadata_command == "get": return self.service.get(args.instance, args.database, args.table)
21
+ payload = json.loads(args.payload)
22
+ if not isinstance(payload, dict): raise ValueError("Metadata payload must be a JSON object.")
23
+ return self.service.update(MetadataUpdateRequest(args.instance, args.database, args.table, payload))
@@ -0,0 +1,2 @@
1
+ """Input DTOs used by CLI controllers and services."""
2
+
@@ -0,0 +1,43 @@
1
+ """Typed command inputs, independent from argparse and core models."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ScopeRequest:
9
+ config_name: Optional[str] = None
10
+ instance_name: Optional[str] = None
11
+ no_cache: bool = False
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class TablesRequest(ScopeRequest):
16
+ schema_name: Optional[str] = None
17
+ limit: Optional[int] = None
18
+ offset: Optional[int] = None
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class DescribeRequest(TablesRequest):
23
+ table_name: Optional[str] = None
24
+ generate_ai_docs: bool = False
25
+ save_metadata: bool = True
26
+ only_if_changed: bool = False
27
+ export_markdown: bool = True
28
+ export_okf: bool = True
29
+ preformat: bool = True
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PageRequest:
34
+ page: int = 1
35
+ page_size: int = 20
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class MetadataUpdateRequest:
40
+ instance_name: str
41
+ database_name: str
42
+ table_name: str
43
+ payload: Dict[str, Any]
@@ -0,0 +1,62 @@
1
+ """CLI composition root and process-level error handling."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any, List, Optional
6
+ from dotenv import load_dotenv
7
+
8
+ from irides_cli.controllers.init_controller import InitController
9
+ from irides_cli.controllers.introspection_controller import IntrospectionController
10
+ from irides_cli.controllers.metadata_controller import MetadataController
11
+ from irides_cli.presentation.parser import build_parser
12
+
13
+
14
+ def _json_default(value: Any) -> Any:
15
+ if hasattr(value, "model_dump"): return value.model_dump(mode="json")
16
+ if hasattr(value, "dict"): return value.dict()
17
+ raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
18
+
19
+
20
+ def print_json(data: Any) -> None:
21
+ print(json.dumps(data, default=_json_default, ensure_ascii=False, indent=2))
22
+
23
+
24
+ def run(args: Any) -> Any:
25
+ if args.command == "init":
26
+ return InitController().execute(fmt=args.format, force=args.force)
27
+ if args.command == "metadata":
28
+ return MetadataController().execute(args)
29
+
30
+ config_file = getattr(args, "config_file", None)
31
+ controller = IntrospectionController(config_file=config_file)
32
+ result = controller.execute(args)
33
+ if args.command == "configurations" and isinstance(result, list) and len(result) == 0:
34
+ print(
35
+ "irides: notice: no database targets configured. Run 'irides init' to generate a configuration template or check your .env.",
36
+ file=sys.stderr,
37
+ )
38
+ return result
39
+
40
+
41
+ def main(argv: Optional[List[str]] = None) -> int:
42
+ args = build_parser().parse_args(argv)
43
+
44
+ env_file = getattr(args, "env_file", None)
45
+ if env_file:
46
+ load_dotenv(dotenv_path=env_file, override=True)
47
+
48
+ try:
49
+ result = run(args)
50
+ if result is None: raise ValueError("No metadata found.")
51
+ print_json(result)
52
+ return 0
53
+ except (ConnectionError, ValueError, json.JSONDecodeError) as error:
54
+ print(f"irides: {error}", file=sys.stderr)
55
+ return 1
56
+ except Exception as error:
57
+ print(f"irides: unexpected error: {error}", file=sys.stderr)
58
+ return 1
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -0,0 +1,2 @@
1
+ """Command-line input and output adapters."""
2
+
@@ -0,0 +1,47 @@
1
+ """Argparse command definitions; no business logic belongs here."""
2
+
3
+ import argparse
4
+
5
+
6
+ def _add_scope_arguments(parser: argparse.ArgumentParser, *, table: bool = False) -> None:
7
+ parser.add_argument("-t", "--target", "--config", dest="config_name", help="Configured database target name")
8
+ parser.add_argument("--instance", dest="instance_name", help="Database instance")
9
+ if table: parser.add_argument("--schema", dest="schema_name", help="Schema/database name")
10
+ parser.add_argument("--no-cache", action="store_true", help="Bypass the Redis cache")
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(prog="irides", description="Inspect databases through Irides core, without the API service.")
15
+ parser.add_argument("-e", "--env-file", dest="env_file", help="Path to .env file to load variables from")
16
+ parser.add_argument("-c", "--config-file", dest="config_file", help="Path to YAML/JSON configuration file (e.g. irides.yaml)")
17
+
18
+ subparsers = parser.add_subparsers(dest="command", required=True)
19
+
20
+ init_cmd = subparsers.add_parser("init", help="Initialize a template configuration file (irides.yaml or .env)")
21
+ init_cmd.add_argument("-f", "--format", choices=["yaml", "env"], default="yaml", help="Config format: yaml (default) or env")
22
+ init_cmd.add_argument("--force", action="store_true", help="Overwrite existing configuration file")
23
+
24
+ subparsers.add_parser("configurations", help="List active configurations")
25
+ connect = subparsers.add_parser("connect", help="Test a configuration")
26
+ connect.add_argument("config_name", metavar="TARGET", help="Database target name to connect to")
27
+ instances = subparsers.add_parser("instances", help="List instances")
28
+ instances.add_argument("-t", "--target", "--config", dest="config_name", help="Configured database target name")
29
+ instances.add_argument("--no-cache", action="store_true")
30
+ schemas = subparsers.add_parser("schemas", help="List schemas"); _add_scope_arguments(schemas)
31
+ tables = subparsers.add_parser("tables", help="List tables"); _add_scope_arguments(tables, table=True); tables.add_argument("--limit", type=int); tables.add_argument("--offset", type=int)
32
+ describe = subparsers.add_parser("describe", help="Describe tables and optionally save metadata")
33
+ _add_scope_arguments(describe, table=True); describe.add_argument("--table", dest="table_name")
34
+ describe.add_argument("--generate-ai-docs", action="store_true"); describe.add_argument("--no-save-metadata", dest="save_metadata", action="store_false", default=True); describe.add_argument("--only-if-changed", action="store_true")
35
+ describe.add_argument("--no-export-markdown", dest="export_markdown", action="store_false", default=True, help="Do not generate the default Markdown export")
36
+ describe.add_argument("--save-markdown", dest="export_markdown", action="store_true", help=argparse.SUPPRESS)
37
+ describe.add_argument("--no-export-okf", dest="export_okf", action="store_false", default=True, help="Do not generate the default OKF bundle")
38
+ describe.add_argument("--no-preformat", dest="preformat", action="store_false", default=True, help="Export the full metadata instead of the essential view")
39
+ metadata = subparsers.add_parser("metadata", help="Read or update stored metadata"); metadata_sub = metadata.add_subparsers(dest="metadata_command", required=True)
40
+ for name in ("instances", "databases", "tables"):
41
+ command = metadata_sub.add_parser(name)
42
+ if name != "instances": command.add_argument("instance")
43
+ if name == "tables": command.add_argument("database")
44
+ command.add_argument("--page", type=int, default=1); command.add_argument("--page-size", type=int, default=20)
45
+ get = metadata_sub.add_parser("get"); get.add_argument("instance"); get.add_argument("database"); get.add_argument("table")
46
+ update = metadata_sub.add_parser("update"); update.add_argument("instance"); update.add_argument("database"); update.add_argument("table"); update.add_argument("payload", help="JSON object to merge into the metadata")
47
+ return parser
@@ -0,0 +1,2 @@
1
+ """Application services for the CLI."""
2
+
@@ -0,0 +1,94 @@
1
+ """Core-backed synchronous database introspection use cases."""
2
+
3
+ import os
4
+ from typing import Any, Dict, Iterable, List
5
+
6
+ from core.db_connector.ai_service import AIDocumentationService
7
+ from core.db_connector.cache_manager import CacheManager
8
+ from core.db_connector.config_service import ConfigService
9
+ from core.db_connector.manager import ConnectorManager
10
+ from core.db_connector.models import Schema, Table
11
+ from core.db_connector.storage import get_metadata_store
12
+ from core.db_connector.exporting import ExportFormat, ExportOptions
13
+
14
+ from irides_cli.dto.requests import DescribeRequest, ScopeRequest, TablesRequest
15
+
16
+
17
+ class IntrospectionService:
18
+ """Runs live introspection without any dependency on API-layer code."""
19
+
20
+ def __init__(self, config_service: ConfigService | None = None, config_file: str | None = None) -> None:
21
+ if config_service is None:
22
+ cache = CacheManager(
23
+ host=os.getenv("REDIS_HOST", "localhost"),
24
+ port=int(os.getenv("REDIS_PORT", "6379")),
25
+ db=int(os.getenv("REDIS_DB", "0")),
26
+ ttl_seconds=int(os.getenv("REDIS_TTL_SECONDS", "86400")),
27
+ socket_connect_timeout=float(os.getenv("REDIS_SOCKET_CONNECT_TIMEOUT_SECONDS", "2")),
28
+ socket_timeout=float(os.getenv("REDIS_CACHE_SOCKET_TIMEOUT_SECONDS", "2")),
29
+ )
30
+ config_service = ConfigService(ConnectorManager(cache), config_file=config_file)
31
+ self.config_service = config_service
32
+
33
+ def configurations(self) -> List[str]:
34
+ return self.config_service.get_available_configurations()
35
+
36
+ def connect(self, config_name: str) -> Dict[str, Any]:
37
+ return self.config_service.test_connection(config_name)
38
+
39
+ def instances(self, request: ScopeRequest) -> List[Any]:
40
+ names = [request.config_name] if request.config_name else self.configurations()
41
+ return [instance for name in names for instance in self.config_service.list_instances(name, no_cache=request.no_cache)]
42
+
43
+ def _scope(self, request: ScopeRequest) -> Iterable[tuple[str, str, Any]]:
44
+ configs = [request.config_name] if request.config_name else self.configurations()
45
+ for config in configs:
46
+ for instance in self.config_service.resolve_instance_names(config, request.instance_name, request.no_cache):
47
+ yield config, instance, self.config_service._get_connector_for_host(config, instance)
48
+
49
+ def schemas(self, request: ScopeRequest) -> List[Any]:
50
+ return [schema for _, instance, connector in self._scope(request) for schema in connector.list_schemas(instance_name=instance, no_cache=request.no_cache)]
51
+
52
+ def tables(self, request: TablesRequest) -> List[Any]:
53
+ found = []
54
+ for _, instance, connector in self._scope(request):
55
+ schemas = [Schema(name=request.schema_name)] if request.schema_name else connector.list_schemas(instance_name=instance, no_cache=request.no_cache)
56
+ for schema in schemas:
57
+ found.extend(connector.list_tables(instance_name=instance, schema_name=schema.name, limit=request.limit, offset=request.offset, no_cache=request.no_cache))
58
+ return found
59
+
60
+ def describe(self, request: DescribeRequest) -> List[Any]:
61
+ results = []
62
+ formats = []
63
+ if request.export_markdown:
64
+ formats.append(ExportFormat.MARKDOWN)
65
+ if request.export_okf:
66
+ formats.append(ExportFormat.OKF)
67
+ export_options = ExportOptions(formats=formats, preformat=request.preformat)
68
+ store = get_metadata_store() if request.save_metadata or formats else None
69
+ ai_service = AIDocumentationService() if request.generate_ai_docs else None
70
+ for config, instance, connector in self._scope(request):
71
+ schemas = [Schema(name=request.schema_name)] if request.schema_name else connector.list_schemas(instance_name=instance, no_cache=request.no_cache)
72
+ for schema in schemas:
73
+ tables = [Table(name=request.table_name, schema_name=schema.name)] if request.table_name else connector.list_tables(instance_name=instance, schema_name=schema.name, no_cache=request.no_cache)
74
+ for table in tables:
75
+ description = connector.describe_table(instance_name=instance, schema_name=schema.name, table_name=table.name, no_cache=request.no_cache)
76
+ schema_description = description.model_dump(exclude={"ai_documentation", "ai_generation_status", "ai_generation_error"})
77
+ ai_documentation = None
78
+ if ai_service:
79
+ ai_documentation = ai_service.generate_table_documentation(schema_description)
80
+ description = description.model_copy(update={"ai_documentation": ai_documentation, "ai_generation_status": "generated" if ai_documentation else "failed", "ai_generation_error": None if ai_documentation else ai_service.last_error})
81
+ results.append(description)
82
+ if store:
83
+ store.save_table_metadata(
84
+ config,
85
+ instance,
86
+ schema.name,
87
+ table.name,
88
+ schema_description,
89
+ ai_documentation,
90
+ only_if_changed=request.only_if_changed,
91
+ export_options=export_options,
92
+ save_metadata=request.save_metadata,
93
+ )
94
+ return results
@@ -0,0 +1,27 @@
1
+ """Operations over metadata persisted by core."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from core.db_connector.storage import BaseMetadataStore, get_metadata_store
6
+
7
+ from irides_cli.dto.requests import MetadataUpdateRequest, PageRequest
8
+
9
+
10
+ class MetadataService:
11
+ def __init__(self, store: Optional[BaseMetadataStore] = None) -> None:
12
+ self.store = store or get_metadata_store()
13
+
14
+ def instances(self, page: PageRequest) -> Dict[str, Any]:
15
+ return self.store.list_instances(page.page, page.page_size)
16
+
17
+ def databases(self, instance_name: str, page: PageRequest) -> Dict[str, Any]:
18
+ return self.store.list_databases(instance_name, page.page, page.page_size)
19
+
20
+ def tables(self, instance_name: str, database_name: str, page: PageRequest) -> Dict[str, Any]:
21
+ return self.store.list_tables_metadata(instance_name, database_name, page.page, page.page_size)
22
+
23
+ def get(self, instance_name: str, database_name: str, table_name: str) -> Optional[Dict[str, Any]]:
24
+ return self.store.find_table_metadata(instance_name, database_name, table_name)
25
+
26
+ def update(self, request: MetadataUpdateRequest) -> Optional[Dict[str, Any]]:
27
+ return self.store.update_table_metadata(request.instance_name, request.database_name, request.table_name, request.payload)
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: irides-cli
3
+ Version: 0.1.0
4
+ Summary: A command-line interface for discovering, inspecting, and managing structured database context for AI systems.
5
+ Author-email: Gian Andrea Sechi <me@gianandreasechi.com>
6
+ License: Apache-2.0
7
+ Project-URL: Source Code, https://github.com/GianAndreaSechi/irides/cli
8
+ Project-URL: Homepage, https://www.gianandreasechi.com
9
+ Keywords: database,introspection,cli,irides,schema
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: irides-core>=0.1.0
24
+
25
+ # Iride CLI
26
+
27
+ Command-line interface for database introspection, built only on the `core` package and Python's standard library. It does not require FastAPI, MCP, or the Redis worker.
28
+
29
+ ## Structure
30
+
31
+ The CLI is organized by responsibility:
32
+
33
+ - `irides_cli/presentation/`: command definitions and `argparse` parsing;
34
+ - `irides_cli/controllers/`: maps command-line arguments to use cases;
35
+ - `irides_cli/dto/`: immutable, typed request DTOs;
36
+ - `irides_cli/services/`: live introspection and metadata operations using only `core`;
37
+ - `irides_cli/main.py`: composition root, JSON serialization, and process-level error handling.
38
+
39
+ ## Local installation
40
+
41
+ ```bash
42
+ pip install -e ./core -e ./cli
43
+ ```
44
+
45
+ ## Quickstart & Configuration
46
+
47
+ Irides supports two ways to configure database targets:
48
+
49
+ ### Option 1: Declarative Config File (`irides.yaml`) — Recommended
50
+ Generate a template configuration with:
51
+ ```bash
52
+ irides init
53
+ ```
54
+ This creates an `irides.yaml` file in the current directory:
55
+ ```yaml
56
+ targets:
57
+ my_postgres:
58
+ type: postgres
59
+ host: localhost
60
+ port: 5432
61
+ user: postgres
62
+ password: "${PG_PASSWORD:-secret}"
63
+ database: my_database
64
+
65
+ local_sqlite:
66
+ type: sqlite
67
+ database: ./app.db
68
+ ```
69
+ You can pass a custom config file anytime using `-c` / `--config-file`:
70
+ ```bash
71
+ irides -c /path/to/my_config.yaml configurations
72
+ ```
73
+
74
+ ### Option 2: Environment Variables (`.env`)
75
+ You can initialize an environment template with:
76
+ ```bash
77
+ irides init --format env
78
+ ```
79
+ Or specify an explicit `.env` file via `-e` / `--env-file`:
80
+ ```bash
81
+ irides -e /path/to/.env configurations
82
+ ```
83
+
84
+ ## Commands
85
+
86
+ ```bash
87
+ # Initialize template configuration
88
+ irides init
89
+
90
+ # List configured database targets
91
+ irides configurations
92
+
93
+ # Test database connection
94
+ irides connect my_postgres
95
+
96
+ # List database instances
97
+ irides instances --target my_postgres
98
+
99
+ # List schemas in a database
100
+ irides schemas --target my_postgres --instance localhost
101
+
102
+ # List tables (supports --limit and --offset)
103
+ irides tables --target my_postgres --instance localhost --schema public --limit 50
104
+
105
+ # Introspect table schema
106
+ irides describe --target my_postgres --instance localhost --schema public --table orders
107
+ irides describe --target my_postgres --instance localhost --schema public --table orders --generate-ai-docs
108
+ irides describe --target my_postgres --instance localhost --schema public --table orders --no-export-okf
109
+ ```
110
+
111
+ > **Note**: `--target` and `--config` are interchangeable aliases.
112
+ Omitting `--target`, `--instance`, `--schema`, or `--table` expands the scope, just like the corresponding API endpoints. Results are always written as JSON to stdout; errors are written to stderr. Add `--no-cache` to introspection commands to bypass Redis.
113
+
114
+ `describe` saves canonical JSON metadata and generates both **Markdown** and **Open Knowledge Format (OKF v0.2)** exports by default.
115
+
116
+ Available options for `describe`:
117
+ - `--generate-ai-docs`: generate domain summary and column descriptions via LiteLLM.
118
+ - `--no-save-metadata`: skip saving the canonical JSON metadata file (exports are still generated if enabled).
119
+ - `--only-if-changed`: skip writing if the schema is identical to the stored version.
120
+ - `--no-export-markdown`: disable the default Markdown export.
121
+ - `--no-export-okf`: disable the default OKF catalog bundle generation.
122
+ - `--no-preformat`: export full metadata instead of the essential deterministic record.
123
+ - `--save-markdown`: legacy compatibility flag for Markdown export.
124
+
125
+ Artifacts are persisted under `STORAGE_EXPORT_DIR` (default `storage/exports`):
126
+ - `storage/exports/markdown/{config}/{instance}/{schema}/{table}.md`
127
+ - `storage/exports/okf/catalog/{config}/{instance}/{schema}/{table}.md` (along with `storage/exports/okf/catalog/index.md`)
128
+
129
+ ## Metadata
130
+
131
+ ```bash
132
+ irides metadata instances
133
+ irides metadata databases db1.company.com
134
+ irides metadata tables db1.company.com production
135
+ irides metadata get db1.company.com production orders
136
+ irides metadata update db1.company.com production orders '{"owner":"data-team","tags":["billing"]}'
137
+ ```
138
+
139
+ The CLI does not include `scan` commands. They are asynchronous and explicitly require Redis Streams and the `worker` service.
140
+
141
+ ## Docker
142
+
143
+ The CLI reads its configuration from `cli/.env` and uses the shared Redis network. Start by copying `.env.example` as shown above and configure at least one `DB_TARGETS` entry.
144
+
145
+ ```bash
146
+ docker compose -f infra/docker-compose.infra.yml up -d
147
+ docker compose -f cli/docker-compose.yml run --rm irides-cli configurations
148
+ docker compose -f cli/docker-compose.yml run --rm irides-cli tables --config sales_mysql --schema public
149
+ docker compose -f cli/docker-compose.yml run --rm irides-cli describe --config sales_mysql --schema public --table orders
150
+ ```
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ irides_cli/__init__.py
5
+ irides_cli/main.py
6
+ irides_cli.egg-info/PKG-INFO
7
+ irides_cli.egg-info/SOURCES.txt
8
+ irides_cli.egg-info/dependency_links.txt
9
+ irides_cli.egg-info/entry_points.txt
10
+ irides_cli.egg-info/requires.txt
11
+ irides_cli.egg-info/top_level.txt
12
+ irides_cli/controllers/__init__.py
13
+ irides_cli/controllers/init_controller.py
14
+ irides_cli/controllers/introspection_controller.py
15
+ irides_cli/controllers/metadata_controller.py
16
+ irides_cli/dto/__init__.py
17
+ irides_cli/dto/requests.py
18
+ irides_cli/presentation/__init__.py
19
+ irides_cli/presentation/parser.py
20
+ irides_cli/services/__init__.py
21
+ irides_cli/services/introspection_service.py
22
+ irides_cli/services/metadata_service.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ irides = irides_cli.main:main
@@ -0,0 +1 @@
1
+ irides-core>=0.1.0
@@ -0,0 +1 @@
1
+ irides_cli
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "irides-cli"
7
+ version = "0.1.0"
8
+ description = "A command-line interface for discovering, inspecting, and managing structured database context for AI systems."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [
13
+ {name = "Gian Andrea Sechi", email = "me@gianandreasechi.com"}
14
+ ]
15
+ keywords = ["database", "introspection", "cli", "irides", "schema"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: Apache Software License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Database",
28
+ ]
29
+ dependencies = [
30
+ "irides-core>=0.1.0",
31
+ ]
32
+
33
+ [project.scripts]
34
+ irides = "irides_cli.main:main"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["."]
38
+ include = ["irides_cli*"]
39
+
40
+ [project.urls]
41
+ "Source Code" = "https://github.com/GianAndreaSechi/irides/cli"
42
+ "Homepage" = "https://www.gianandreasechi.com"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name="irides-cli",
5
+ version="0.1.0",
6
+ description="Command-line interface for Irides database introspection",
7
+ packages=find_packages(include=["irides_cli*"]),
8
+ install_requires=["irides-core>=0.1.0"],
9
+ entry_points={"console_scripts": ["irides=irides_cli.main:main"]},
10
+ )