sqlseed-cli 0.2.4__tar.gz → 0.2.5__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.
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/PKG-INFO +12 -7
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/README.md +10 -5
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/pyproject.toml +1 -1
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/src/sqlseed_cli/main.py +33 -24
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_cli_ai_commands.py +7 -5
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_config_execution_safety.py +71 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/.gitignore +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/LICENSE +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/src/sqlseed_cli/__init__.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/src/sqlseed_cli/_utils.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/.pylintrc +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/__init__.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/conftest.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_cli.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_cli_utils.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_cli_yaml_priority.py +0 -0
- {sqlseed_cli-0.2.4 → sqlseed_cli-0.2.5}/tests/test_snapshot_transform.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlseed-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Command-line interface for sqlseed test data generation, preview and configuration replay
|
|
5
5
|
Project-URL: Documentation, https://sunbos.github.io/sqlseed/guide/
|
|
6
6
|
Project-URL: Homepage, https://github.com/sunbos/sqlseed
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Requires-Dist: click>=8.0
|
|
20
20
|
Requires-Dist: rich>=13.0
|
|
21
|
-
Requires-Dist: sqlseed<0.3,>=0.2.
|
|
21
|
+
Requires-Dist: sqlseed<0.3,>=0.2.5.dev0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# sqlseed-cli
|
|
@@ -29,13 +29,15 @@ configuration. This package provides the `sqlseed` command.
|
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
These instructions target version 0.2.5. Check [Releases](https://github.com/sunbos/sqlseed/releases)
|
|
33
|
+
for publication status; use the source installation below to test an unpublished candidate.
|
|
34
|
+
Use a Python 3.10+ virtual environment:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
python -m pip install "sqlseed-cli==0.2.
|
|
37
|
+
python -m pip install "sqlseed-cli==0.2.5"
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
Core 0.2.
|
|
40
|
+
Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
|
|
39
41
|
For development, install Core and the required local plugins together from the
|
|
40
42
|
repository root:
|
|
41
43
|
|
|
@@ -77,7 +79,7 @@ sqlseed replay <cache_dir>/snapshots/YYYY-MM-DD_HHMMSS_ffffff_users.yaml
|
|
|
77
79
|
For PostgreSQL, install the Core `postgres` extra and use `--url`:
|
|
78
80
|
|
|
79
81
|
```bash
|
|
80
|
-
python -m pip install "sqlseed[postgres]==0.2.
|
|
82
|
+
python -m pip install "sqlseed[postgres]==0.2.5" "sqlseed-cli==0.2.5"
|
|
81
83
|
sqlseed fill --url "postgresql+psycopg://user:pass@host/db" -t users -n 1000 --provider faker --no-ai
|
|
82
84
|
sqlseed inspect --url "postgresql+psycopg://user:pass@host/db"
|
|
83
85
|
```
|
|
@@ -105,8 +107,11 @@ The optional AI plugin registers `ai-suggest`, `ai-analyze`, and `auto-heal` thr
|
|
|
105
107
|
|
|
106
108
|
## Requirements
|
|
107
109
|
|
|
110
|
+
These metadata requirements apply to version 0.2.5 and its source candidates.
|
|
111
|
+
Use local Core and plugins together when developing from source.
|
|
112
|
+
|
|
108
113
|
- Python `>=3.10`
|
|
109
|
-
- `sqlseed>=0.2.
|
|
114
|
+
- `sqlseed>=0.2.5.dev0,<0.3`
|
|
110
115
|
- `click>=8.0`
|
|
111
116
|
- `rich>=13.0`
|
|
112
117
|
|
|
@@ -6,13 +6,15 @@ configuration. This package provides the `sqlseed` command.
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
These instructions target version 0.2.5. Check [Releases](https://github.com/sunbos/sqlseed/releases)
|
|
10
|
+
for publication status; use the source installation below to test an unpublished candidate.
|
|
11
|
+
Use a Python 3.10+ virtual environment:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
python -m pip install "sqlseed-cli==0.2.
|
|
14
|
+
python -m pip install "sqlseed-cli==0.2.5"
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Core 0.2.
|
|
17
|
+
Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
|
|
16
18
|
For development, install Core and the required local plugins together from the
|
|
17
19
|
repository root:
|
|
18
20
|
|
|
@@ -54,7 +56,7 @@ sqlseed replay <cache_dir>/snapshots/YYYY-MM-DD_HHMMSS_ffffff_users.yaml
|
|
|
54
56
|
For PostgreSQL, install the Core `postgres` extra and use `--url`:
|
|
55
57
|
|
|
56
58
|
```bash
|
|
57
|
-
python -m pip install "sqlseed[postgres]==0.2.
|
|
59
|
+
python -m pip install "sqlseed[postgres]==0.2.5" "sqlseed-cli==0.2.5"
|
|
58
60
|
sqlseed fill --url "postgresql+psycopg://user:pass@host/db" -t users -n 1000 --provider faker --no-ai
|
|
59
61
|
sqlseed inspect --url "postgresql+psycopg://user:pass@host/db"
|
|
60
62
|
```
|
|
@@ -82,8 +84,11 @@ The optional AI plugin registers `ai-suggest`, `ai-analyze`, and `auto-heal` thr
|
|
|
82
84
|
|
|
83
85
|
## Requirements
|
|
84
86
|
|
|
87
|
+
These metadata requirements apply to version 0.2.5 and its source candidates.
|
|
88
|
+
Use local Core and plugins together when developing from source.
|
|
89
|
+
|
|
85
90
|
- Python `>=3.10`
|
|
86
|
-
- `sqlseed>=0.2.
|
|
91
|
+
- `sqlseed>=0.2.5.dev0,<0.3`
|
|
87
92
|
- `click>=8.0`
|
|
88
93
|
- `rich>=13.0`
|
|
89
94
|
|
|
@@ -9,7 +9,6 @@ AI-related commands (e.g. ai-suggest) are discovered via the
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
-
import re
|
|
13
12
|
from dataclasses import dataclass
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from typing import Any
|
|
@@ -24,26 +23,25 @@ from sqlseed import fill_from_config
|
|
|
24
23
|
from sqlseed import preview as api_preview
|
|
25
24
|
from sqlseed._utils.logger import configure_logging, get_logger
|
|
26
25
|
from sqlseed._utils.paths import get_cache_dir
|
|
26
|
+
from sqlseed._utils.redaction import redact_url_credentials
|
|
27
27
|
from sqlseed._version import __version__
|
|
28
28
|
from sqlseed.config.loader import generate_template, load_config, save_config
|
|
29
29
|
from sqlseed.config.models import GeneratorConfig, ProviderType, TableConfig
|
|
30
30
|
from sqlseed.config.snapshot import SnapshotManager
|
|
31
31
|
from sqlseed.core.orchestrator import DataOrchestrator
|
|
32
|
+
from sqlseed.generators import ConfigurationError
|
|
32
33
|
|
|
33
34
|
CONFLICTING_DATABASE_OPTIONS = "Cannot specify both positional db_path and --url. Use one or the other."
|
|
34
35
|
|
|
35
36
|
logger = get_logger(__name__)
|
|
36
37
|
|
|
37
|
-
# Redact the username and password in database URLs before displaying errors.
|
|
38
|
-
_CREDENTIAL_PATTERN = re.compile(r"://[^:@/\s]+:[^@/\s]+@")
|
|
39
|
-
|
|
40
38
|
|
|
41
39
|
def _redact_credentials(text: str) -> str:
|
|
42
40
|
"""Redact credentials in URLs to prevent leaking secrets in error messages.
|
|
43
41
|
|
|
44
42
|
Replaces the ``user:pass@`` segment of a URL with ``***:***@``.
|
|
45
43
|
"""
|
|
46
|
-
return
|
|
44
|
+
return redact_url_credentials(text)
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
@click.group()
|
|
@@ -273,7 +271,10 @@ def fill(**kwargs: Any) -> None:
|
|
|
273
271
|
config_path=config_path,
|
|
274
272
|
transform_path=transform_path,
|
|
275
273
|
)
|
|
276
|
-
|
|
274
|
+
try:
|
|
275
|
+
_execute_fill(options)
|
|
276
|
+
except ConfigurationError as exc:
|
|
277
|
+
raise click.UsageError(_redact_credentials(str(exc))) from None
|
|
277
278
|
|
|
278
279
|
|
|
279
280
|
def _execute_config_fill(options: FillOptions, config_path: str) -> None:
|
|
@@ -315,7 +316,12 @@ def _execute_fill(options: FillOptions) -> None:
|
|
|
315
316
|
if not (fill_db_path or fill_url):
|
|
316
317
|
raise click.UsageError("db_path or --url is required when not using --config")
|
|
317
318
|
|
|
318
|
-
logger.debug(
|
|
319
|
+
logger.debug(
|
|
320
|
+
"Starting fill",
|
|
321
|
+
target=redact_url_credentials(fill_url or fill_db_path or ""),
|
|
322
|
+
table=options.table,
|
|
323
|
+
count=effective_count,
|
|
324
|
+
)
|
|
319
325
|
|
|
320
326
|
try:
|
|
321
327
|
result = api_fill(
|
|
@@ -333,12 +339,12 @@ def _execute_fill(options: FillOptions) -> None:
|
|
|
333
339
|
skip_ai=options.flags.no_ai,
|
|
334
340
|
)
|
|
335
341
|
except ValueError as exc:
|
|
336
|
-
logger.debug("Fill failed with ValueError", error=str(exc))
|
|
342
|
+
logger.debug("Fill failed with ValueError", error=_redact_credentials(str(exc)))
|
|
337
343
|
raise click.UsageError(_redact_credentials(str(exc))) from exc
|
|
338
344
|
click.echo(str(result))
|
|
339
345
|
if result.errors:
|
|
340
346
|
for err in result.errors:
|
|
341
|
-
click.echo(f" Warning: {err}", err=True)
|
|
347
|
+
click.echo(f" Warning: {_redact_credentials(err)}", err=True)
|
|
342
348
|
|
|
343
349
|
if options.flags.snapshot:
|
|
344
350
|
_save_snapshot_cmd(
|
|
@@ -407,8 +413,8 @@ def preview(
|
|
|
407
413
|
locale=locale,
|
|
408
414
|
seed=seed,
|
|
409
415
|
)
|
|
410
|
-
except (ValueError, RuntimeError, OSError) as exc:
|
|
411
|
-
logger.debug("Preview failed", error=str(exc))
|
|
416
|
+
except (ValueError, RuntimeError, OSError, ConfigurationError) as exc:
|
|
417
|
+
logger.debug("Preview failed", error=_redact_credentials(str(exc)))
|
|
412
418
|
raise click.UsageError(_redact_credentials(str(exc))) from exc
|
|
413
419
|
|
|
414
420
|
if not rows:
|
|
@@ -506,7 +512,7 @@ def inspect(db_path: str | None, table: str | None, show_mapping: bool, db_url:
|
|
|
506
512
|
for tbl in tables:
|
|
507
513
|
_inspect_table(orch, tbl, show_mapping, console)
|
|
508
514
|
except (ValueError, RuntimeError, OSError) as exc:
|
|
509
|
-
logger.debug("Inspect failed", error=str(exc))
|
|
515
|
+
logger.debug("Inspect failed", error=_redact_credentials(str(exc)))
|
|
510
516
|
raise click.UsageError(_redact_credentials(str(exc))) from exc
|
|
511
517
|
|
|
512
518
|
|
|
@@ -544,7 +550,7 @@ def init(config_path: str, db: str | None, db_url: str | None) -> None:
|
|
|
544
550
|
config = generate_template(db_path=effective_db, url=db_url)
|
|
545
551
|
save_config(config, config_path)
|
|
546
552
|
except (ValueError, RuntimeError, OSError) as exc:
|
|
547
|
-
logger.debug("Init failed", error=str(exc))
|
|
553
|
+
logger.debug("Init failed", error=_redact_credentials(str(exc)))
|
|
548
554
|
raise click.UsageError(_redact_credentials(str(exc))) from exc
|
|
549
555
|
click.echo(f"Configuration template saved to: {config_path}")
|
|
550
556
|
|
|
@@ -593,20 +599,23 @@ def replay(snapshot_path: str) -> None:
|
|
|
593
599
|
table_config = tc
|
|
594
600
|
break
|
|
595
601
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
602
|
+
try:
|
|
603
|
+
with DataOrchestrator.from_config(config) as orch:
|
|
604
|
+
result = orch.fill_table(
|
|
605
|
+
table_name=table_name,
|
|
606
|
+
count=count,
|
|
607
|
+
seed=seed,
|
|
608
|
+
batch_size=table_config.batch_size if table_config else 5000,
|
|
609
|
+
clear_before=table_config.clear_before if table_config else False,
|
|
610
|
+
column_configs=table_config.columns if table_config else None,
|
|
611
|
+
transform=table_config.transform if table_config else None,
|
|
612
|
+
)
|
|
613
|
+
except (ValueError, RuntimeError, OSError, ConfigurationError) as exc:
|
|
614
|
+
raise click.UsageError(_redact_credentials(str(exc))) from None
|
|
606
615
|
click.echo(str(result))
|
|
607
616
|
if result.errors:
|
|
608
617
|
for err in result.errors:
|
|
609
|
-
click.echo(f" Warning: {err}", err=True)
|
|
618
|
+
click.echo(f" Warning: {_redact_credentials(err)}", err=True)
|
|
610
619
|
raise SystemExit(1)
|
|
611
620
|
|
|
612
621
|
|
|
@@ -822,14 +822,16 @@ class TestReportAIFailure:
|
|
|
822
822
|
_, kwargs = mock_echo.call_args
|
|
823
823
|
assert kwargs.get("err") is True
|
|
824
824
|
|
|
825
|
-
def
|
|
826
|
-
"""The
|
|
825
|
+
def test_message_preserves_the_failure_without_unrelated_service_advice(self) -> None:
|
|
826
|
+
"""The summary does not replace a specific error with stale advice."""
|
|
827
827
|
with patch("click.echo") as mock_echo, pytest.raises(SystemExit):
|
|
828
828
|
_report_ai_failure()
|
|
829
829
|
message = str(mock_echo.call_args[0][0])
|
|
830
|
-
assert "
|
|
831
|
-
assert "
|
|
832
|
-
assert "
|
|
830
|
+
assert "No suggestions received" in message
|
|
831
|
+
assert "No output file was written" in message
|
|
832
|
+
assert "deepseek" not in message.lower()
|
|
833
|
+
assert "openai" not in message.lower()
|
|
834
|
+
assert "timeout" not in message.lower()
|
|
833
835
|
|
|
834
836
|
|
|
835
837
|
# ---------------------------------------------------------------------------
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
5
8
|
from typing import TYPE_CHECKING
|
|
6
9
|
|
|
7
10
|
import pytest
|
|
@@ -17,6 +20,74 @@ if TYPE_CHECKING:
|
|
|
17
20
|
from pathlib import Path
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
def test_cli_invalid_url_diagnostics_hide_userinfo_and_query_password() -> None:
|
|
24
|
+
target = "sqlite://audit-user:synthetic-userinfo@host/db?password=synthetic-query&timeout=3"
|
|
25
|
+
result = CliRunner().invoke(cli, ["preview", "--url", target, "--table", "items"])
|
|
26
|
+
assert result.exit_code != 0
|
|
27
|
+
assert "Invalid database URL" in result.output
|
|
28
|
+
assert "synthetic-userinfo" not in result.output
|
|
29
|
+
assert "synthetic-query" not in result.output
|
|
30
|
+
assert "host/db?password=***&timeout=3" in result.output
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_debug_fill_does_not_log_raw_connection_credentials() -> None:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
[
|
|
36
|
+
sys.executable,
|
|
37
|
+
"-c",
|
|
38
|
+
"from sqlseed_cli.main import main; main()",
|
|
39
|
+
"fill",
|
|
40
|
+
"--url",
|
|
41
|
+
"sqlite://audit-user:synthetic-userinfo@host/db?password=first-secret second-secret&timeout=3",
|
|
42
|
+
"--table",
|
|
43
|
+
"items",
|
|
44
|
+
"--count",
|
|
45
|
+
"1",
|
|
46
|
+
"--provider",
|
|
47
|
+
"base",
|
|
48
|
+
],
|
|
49
|
+
env={**os.environ, "SQLSEED_LOG_LEVEL": "DEBUG"},
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=30,
|
|
53
|
+
check=False,
|
|
54
|
+
)
|
|
55
|
+
assert result.returncode == 2
|
|
56
|
+
assert "Starting fill" in result.stderr
|
|
57
|
+
assert "Invalid database URL" in result.stderr
|
|
58
|
+
assert all(
|
|
59
|
+
secret not in result.stderr + result.stdout
|
|
60
|
+
for secret in ("synthetic-userinfo", "first-secret", "second-secret")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_invalid_date_config_is_an_actionable_cli_error_without_appending(tmp_path: Path) -> None:
|
|
65
|
+
database = tmp_path / "invalid-date.db"
|
|
66
|
+
with sqlite_connection(database) as conn:
|
|
67
|
+
conn.execute("CREATE TABLE events (created_at DATE NOT NULL)")
|
|
68
|
+
conn.execute("INSERT INTO events VALUES ('2020-01-01')")
|
|
69
|
+
document = {
|
|
70
|
+
"db_path": str(database),
|
|
71
|
+
"provider": "base",
|
|
72
|
+
"tables": [
|
|
73
|
+
{
|
|
74
|
+
"name": "events",
|
|
75
|
+
"count": 5,
|
|
76
|
+
"columns": [{"name": "created_at", "generator": "date", "params": {"end_date": "now"}}],
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
config = tmp_path / "invalid.yaml"
|
|
81
|
+
config.write_text(yaml.safe_dump(document), encoding="utf-8")
|
|
82
|
+
result = CliRunner().invoke(cli, ["fill", "--config", str(config), "--no-ai"])
|
|
83
|
+
assert result.exit_code == 2, result.output
|
|
84
|
+
assert "created_at" in result.output
|
|
85
|
+
assert "YYYY-MM-DD" in result.output
|
|
86
|
+
assert "Traceback" not in result.output
|
|
87
|
+
with sqlite_connection(database) as conn:
|
|
88
|
+
assert conn.execute("SELECT * FROM events").fetchall() == [("2020-01-01",)]
|
|
89
|
+
|
|
90
|
+
|
|
20
91
|
@pytest.mark.parametrize("target_kind", ["path", "url"])
|
|
21
92
|
def test_config_rejects_explicit_target_without_changing_either_database(tmp_path: Path, target_kind: str) -> None:
|
|
22
93
|
configured_db = tmp_path / "configured.db"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|