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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polygres-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command-line interface for Polygres
5
5
  Project-URL: Homepage, https://polygres.com
6
6
  Project-URL: Documentation, https://docs.evokoa.com/polygres
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polygres-cli"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "Command-line interface for Polygres"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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": json.dumps(columns)})
1050
- started = ctx.client.start_csv_import(project_id, file_path, import_fields)
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.0"
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, file: Path, fields: dict[str, str]
124
+ self, project_id: str, fields: dict[str, Any]
125
125
  ) -> dict[str, Any]:
126
- with file.open("rb") as handle:
127
- return self._multipart(
128
- "POST",
129
- f"/projects/{project_id}/imports/csv",
130
- handle,
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 json.JSONDecodeError:
394
+ except jsonlib.JSONDecodeError:
396
395
  return {}
397
396
  return payload if isinstance(payload, dict) else {}
398
397
 
@@ -16,6 +16,7 @@ LOCAL_DEPENDENCY = 9
16
16
 
17
17
  HTTP_EXIT_CODES = {
18
18
  400: USAGE,
19
+ 413: USAGE,
19
20
  401: AUTH,
20
21
  403: PERMISSION,
21
22
  404: NOT_FOUND,
@@ -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.0"
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 UTC, datetime, timedelta
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.0"
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.0"
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
- import_body = import_route.calls[0].request.content
1114
+ import_payload = json.loads(import_route.calls[0].request.content)
1115
1115
  assert b'name="sample_row_count"' in preview_body
1116
- assert b'name="sample_row_count"' not in import_body
1117
- assert b'name="job_id"' in import_body
1118
- assert b'name="columns"' in import_body
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.0"}
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(UTC) + timedelta(minutes=5)).isoformat().replace("+00:00", "Z")
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=UTC)
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