polygres-cli 0.1.0__tar.gz → 0.1.1__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.
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/PKG-INFO +1 -1
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/pyproject.toml +1 -1
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli.py +4 -20
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_client.py +12 -13
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_errors.py +1 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/tests/heavy/test_cli_installed_staging.py +1 -1
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/tests/test_cli.py +55 -10
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/.github/workflows/ci.yml +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/.github/workflows/publish-python-cli.yml +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/.gitignore +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/CONTRIBUTING.md +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/LICENSE +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/README.md +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/SECURITY.md +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/__init__.py +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_auth.py +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_config.py +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_output.py +0 -0
- {polygres_cli-0.1.0 → polygres_cli-0.1.1}/src/polygres_cli/cli_secrets.py +0 -0
|
@@ -45,7 +45,6 @@ UUID_LIKE_RE = re.compile(
|
|
|
45
45
|
)
|
|
46
46
|
SQL_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
47
47
|
MIGRATION_NAME_RE = SQL_IDENTIFIER_RE
|
|
48
|
-
MAX_CSV_UPLOAD_BYTES = 1024**3
|
|
49
48
|
GRAPH_CONFIGURATION_KEYS = {
|
|
50
49
|
"registered_tables",
|
|
51
50
|
"registered_relationships",
|
|
@@ -992,19 +991,6 @@ def handle_text_configs_delete(ctx: Context, args: argparse.Namespace) -> int:
|
|
|
992
991
|
|
|
993
992
|
def handle_import_csv(ctx: Context, args: argparse.Namespace) -> int:
|
|
994
993
|
file_path = _readable_file(args.file)
|
|
995
|
-
try:
|
|
996
|
-
file_size = file_path.stat().st_size
|
|
997
|
-
except OSError as exc:
|
|
998
|
-
raise CliError(
|
|
999
|
-
"VALIDATION_ERROR", f"Unable to read file: {file_path}", exit_code=USAGE
|
|
1000
|
-
) from exc
|
|
1001
|
-
if file_size > MAX_CSV_UPLOAD_BYTES:
|
|
1002
|
-
raise CliError(
|
|
1003
|
-
"IMPORT_LIMIT_EXCEEDED",
|
|
1004
|
-
"CSV file exceeds the 1 GiB local upload limit.",
|
|
1005
|
-
exit_code=USAGE,
|
|
1006
|
-
details={"limit_bytes": MAX_CSV_UPLOAD_BYTES, "file_size_bytes": file_size},
|
|
1007
|
-
)
|
|
1008
994
|
_validate_identifiers(args.schema, args.table)
|
|
1009
995
|
preview_fields = {
|
|
1010
996
|
"target_schema": args.schema,
|
|
@@ -1032,22 +1018,20 @@ def handle_import_csv(ctx: Context, args: argparse.Namespace) -> int:
|
|
|
1032
1018
|
columns = preview_payload.get("columns")
|
|
1033
1019
|
if not isinstance(columns, list):
|
|
1034
1020
|
raise CliError("IMPORT_INVALID", "CSV preview response did not include columns.")
|
|
1035
|
-
import_fields = {
|
|
1021
|
+
import_fields: dict[str, object] = {
|
|
1036
1022
|
"target_schema": args.schema,
|
|
1037
1023
|
"target_table": args.table,
|
|
1038
1024
|
"mode": args.mode,
|
|
1039
1025
|
"encoding": str(preview_payload.get("encoding", args.encoding)),
|
|
1040
1026
|
"delimiter": str(preview_payload.get("delimiter", args.delimiter or "")),
|
|
1041
1027
|
"quote_char": str(preview_payload.get("quote_char", args.quote_char or '"')),
|
|
1042
|
-
"has_header": (
|
|
1043
|
-
"true" if preview_payload.get("has_header", not args.no_header) else "false"
|
|
1044
|
-
),
|
|
1028
|
+
"has_header": bool(preview_payload.get("has_header", not args.no_header)),
|
|
1045
1029
|
}
|
|
1046
1030
|
effective_escape = preview_payload.get("escape_char", args.escape_char)
|
|
1047
1031
|
if effective_escape is not None:
|
|
1048
1032
|
import_fields["escape_char"] = str(effective_escape)
|
|
1049
|
-
import_fields.update({"job_id": job_id, "columns":
|
|
1050
|
-
started = ctx.client.start_csv_import(project_id,
|
|
1033
|
+
import_fields.update({"job_id": job_id, "columns": columns})
|
|
1034
|
+
started = ctx.client.start_csv_import(project_id, import_fields)
|
|
1051
1035
|
output = _import_output(started)
|
|
1052
1036
|
if args.wait and output["import"].get("status") not in {"succeeded", "failed", "cancelled"}:
|
|
1053
1037
|
import_id = output["import"].get("id")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
3
|
+
import json as jsonlib
|
|
4
4
|
import time
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from email.utils import parsedate_to_datetime
|
|
@@ -13,7 +13,7 @@ import httpx
|
|
|
13
13
|
from polygres_cli.cli_errors import AUTH, UNAVAILABLE, CliError, api_error_from_response
|
|
14
14
|
from polygres_cli.cli_secrets import redact_string
|
|
15
15
|
|
|
16
|
-
VERSION = "0.1.
|
|
16
|
+
VERSION = "0.1.1"
|
|
17
17
|
RETRY_STATUSES = {408, 429, 500, 502, 503, 504}
|
|
18
18
|
HEAVY_REQUEST_TIMEOUT = 120.0
|
|
19
19
|
|
|
@@ -121,16 +121,13 @@ class CliControlPlaneClient:
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
def start_csv_import(
|
|
124
|
-
self, project_id: str,
|
|
124
|
+
self, project_id: str, fields: dict[str, Any]
|
|
125
125
|
) -> dict[str, Any]:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
file.name,
|
|
132
|
-
fields,
|
|
133
|
-
)
|
|
126
|
+
return self._post(
|
|
127
|
+
f"/projects/{project_id}/imports/csv",
|
|
128
|
+
fields,
|
|
129
|
+
timeout=HEAVY_REQUEST_TIMEOUT,
|
|
130
|
+
)
|
|
134
131
|
|
|
135
132
|
def list_imports(self, project_id: str) -> dict[str, Any]:
|
|
136
133
|
return self._get(f"/projects/{project_id}/imports")
|
|
@@ -292,10 +289,12 @@ class CliControlPlaneClient:
|
|
|
292
289
|
try:
|
|
293
290
|
request_kwargs: dict[str, Any] = {
|
|
294
291
|
"headers": headers,
|
|
295
|
-
"json": json,
|
|
296
292
|
"data": data,
|
|
297
293
|
"files": files,
|
|
298
294
|
}
|
|
295
|
+
if json is not None:
|
|
296
|
+
headers["Content-Type"] = "application/json"
|
|
297
|
+
request_kwargs["content"] = jsonlib.dumps(json)
|
|
299
298
|
request_timeout = timeout
|
|
300
299
|
if remaining is not None:
|
|
301
300
|
request_timeout = min(request_timeout or self._timeout, remaining)
|
|
@@ -392,7 +391,7 @@ def _json_payload(response: httpx.Response) -> dict[str, Any]:
|
|
|
392
391
|
return {}
|
|
393
392
|
try:
|
|
394
393
|
payload = response.json()
|
|
395
|
-
except
|
|
394
|
+
except jsonlib.JSONDecodeError:
|
|
396
395
|
return {}
|
|
397
396
|
return payload if isinstance(payload, dict) else {}
|
|
398
397
|
|
|
@@ -31,7 +31,7 @@ def test_installed_cli_staging_whoami_contract(tmp_path: Path) -> None:
|
|
|
31
31
|
[executable, "--version"], env=env, text=True, capture_output=True, check=False
|
|
32
32
|
)
|
|
33
33
|
assert version.returncode == 0
|
|
34
|
-
assert version.stdout.strip() == "polygres 0.1.
|
|
34
|
+
assert version.stdout.strip() == "polygres 0.1.1"
|
|
35
35
|
|
|
36
36
|
whoami = subprocess.run(
|
|
37
37
|
[executable, "--json", "whoami"],
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import stat
|
|
6
6
|
import subprocess
|
|
7
|
-
from datetime import
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
@@ -93,7 +93,7 @@ def test_version_and_config_path_json(
|
|
|
93
93
|
) -> None:
|
|
94
94
|
rc, out, _ = run_cli(["--version"], capsys, monkeypatch, tmp_path)
|
|
95
95
|
assert rc == 0
|
|
96
|
-
assert out.strip() == "polygres 0.1.
|
|
96
|
+
assert out.strip() == "polygres 0.1.1"
|
|
97
97
|
|
|
98
98
|
rc, out, _ = run_cli(["--json", "config", "path"], capsys, monkeypatch, tmp_path)
|
|
99
99
|
assert rc == 0
|
|
@@ -430,7 +430,7 @@ def test_projects_list_uses_env_token_and_selected_project_json(
|
|
|
430
430
|
assert err == ""
|
|
431
431
|
assert route.called
|
|
432
432
|
assert route.calls[0].request.headers["Authorization"] == f"Bearer {ACCESS_TOKEN}"
|
|
433
|
-
assert route.calls[0].request.headers["User-Agent"] == "polygres-cli/0.1.
|
|
433
|
+
assert route.calls[0].request.headers["User-Agent"] == "polygres-cli/0.1.1"
|
|
434
434
|
assert json.loads(out) == {
|
|
435
435
|
"projects": [{"id": PROJECT_ID, "name": "Support", "status": "ready"}],
|
|
436
436
|
"selected_project_id": PROJECT_ID,
|
|
@@ -1111,14 +1111,57 @@ def test_import_csv_sends_sample_row_count_only_to_preview(
|
|
|
1111
1111
|
assert preview_route.called
|
|
1112
1112
|
assert import_route.called
|
|
1113
1113
|
preview_body = preview_route.calls[0].request.content
|
|
1114
|
-
|
|
1114
|
+
import_payload = json.loads(import_route.calls[0].request.content)
|
|
1115
1115
|
assert b'name="sample_row_count"' in preview_body
|
|
1116
|
-
assert
|
|
1117
|
-
assert
|
|
1118
|
-
assert
|
|
1116
|
+
assert import_route.calls[0].request.headers["content-type"] == "application/json"
|
|
1117
|
+
assert import_payload["job_id"] == IMPORT_ID
|
|
1118
|
+
assert [column["name"] for column in import_payload["columns"]] == ["id", "title"]
|
|
1119
|
+
assert str(csv_path).encode() not in import_route.calls[0].request.content
|
|
1119
1120
|
assert json.loads(out)["import"]["status"] == "succeeded"
|
|
1120
1121
|
|
|
1121
1122
|
|
|
1123
|
+
@ROUTE_CTX
|
|
1124
|
+
def test_import_csv_preserves_tier_limit_error_and_skips_final_submission(
|
|
1125
|
+
capsys: pytest.CaptureFixture[str],
|
|
1126
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1127
|
+
tmp_path: Path,
|
|
1128
|
+
) -> None:
|
|
1129
|
+
write_config(tmp_path, {"version": 1, "selected_project_id": PROJECT_ID})
|
|
1130
|
+
csv_path = tmp_path / "documents.csv"
|
|
1131
|
+
csv_path.write_text("id,title\n1,Hello\n", encoding="utf-8")
|
|
1132
|
+
preview_route = _stub(
|
|
1133
|
+
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv/preview"),
|
|
1134
|
+
return_value=httpx.Response(
|
|
1135
|
+
413,
|
|
1136
|
+
json={
|
|
1137
|
+
"request_id": "req_import_limit",
|
|
1138
|
+
"error": {
|
|
1139
|
+
"code": "IMPORT_LIMIT_EXCEEDED",
|
|
1140
|
+
"message": "File exceeds the project tier storage limit.",
|
|
1141
|
+
"details": {"limit_bytes": 8, "source": "storage_bytes"},
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
),
|
|
1145
|
+
)
|
|
1146
|
+
import_route = _stub(respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"))
|
|
1147
|
+
|
|
1148
|
+
rc, out, err = run_cli(
|
|
1149
|
+
["--json", "import", "csv", str(csv_path), "--table", "documents"],
|
|
1150
|
+
capsys,
|
|
1151
|
+
monkeypatch,
|
|
1152
|
+
tmp_path,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
assert rc == 2
|
|
1156
|
+
assert err == ""
|
|
1157
|
+
payload = json.loads(out)
|
|
1158
|
+
assert payload["error"]["code"] == "IMPORT_LIMIT_EXCEEDED"
|
|
1159
|
+
assert payload["error"]["details"] == {"limit_bytes": 8, "source": "storage_bytes"}
|
|
1160
|
+
assert payload["request_id"] == "req_import_limit"
|
|
1161
|
+
assert preview_route.call_count == 1
|
|
1162
|
+
assert import_route.call_count == 0
|
|
1163
|
+
|
|
1164
|
+
|
|
1122
1165
|
@ROUTE_CTX
|
|
1123
1166
|
def test_import_status_without_jobs_returns_successful_empty_result(
|
|
1124
1167
|
capsys: pytest.CaptureFixture[str],
|
|
@@ -1803,7 +1846,7 @@ def test_login_browser_fallback_polls_stores_tokens_and_redacts_output(
|
|
|
1803
1846
|
assert ACCESS_TOKEN not in out
|
|
1804
1847
|
assert REFRESH_TOKEN not in out
|
|
1805
1848
|
assert json.loads(start_route.calls[0].request.content) == {
|
|
1806
|
-
"client": {"name": "polygres-cli", "version": "0.1.
|
|
1849
|
+
"client": {"name": "polygres-cli", "version": "0.1.1"}
|
|
1807
1850
|
}
|
|
1808
1851
|
assert json.loads(poll_route.calls[0].request.content) == {
|
|
1809
1852
|
"login_session_id": "cls_abcdefghijklmnopqrstuvwxyz",
|
|
@@ -2590,7 +2633,9 @@ def test_quiet_login_prints_essential_fallback_to_stderr_when_browser_fails(
|
|
|
2590
2633
|
monkeypatch: pytest.MonkeyPatch,
|
|
2591
2634
|
tmp_path: Path,
|
|
2592
2635
|
) -> None:
|
|
2593
|
-
expires = (datetime.now(
|
|
2636
|
+
expires = (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat().replace(
|
|
2637
|
+
"+00:00", "Z"
|
|
2638
|
+
)
|
|
2594
2639
|
_stub(
|
|
2595
2640
|
respx.post(f"{API_BASE_URL}/cli/auth/start"),
|
|
2596
2641
|
return_value=httpx.Response(
|
|
@@ -2688,7 +2733,7 @@ def test_config_save_fails_closed_when_owner_only_permissions_cannot_be_set(
|
|
|
2688
2733
|
def test_retry_after_http_date_is_parsed_as_non_negative_seconds(
|
|
2689
2734
|
monkeypatch: pytest.MonkeyPatch,
|
|
2690
2735
|
) -> None:
|
|
2691
|
-
now = datetime(2026, 7, 9, 12, 0, tzinfo=
|
|
2736
|
+
now = datetime(2026, 7, 9, 12, 0, tzinfo=timezone.utc)
|
|
2692
2737
|
monkeypatch.setattr(cli_client.time, "time", lambda: now.timestamp())
|
|
2693
2738
|
|
|
2694
2739
|
delay = cli_client._retry_after_seconds("Thu, 09 Jul 2026 12:00:05 GMT")
|
|
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
|
|
File without changes
|