polygres-cli 0.1.1__tar.gz → 0.1.2__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.1 → polygres_cli-0.1.2}/PKG-INFO +1 -1
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/pyproject.toml +1 -1
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_client.py +119 -31
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/tests/heavy/test_cli_installed_staging.py +1 -1
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/tests/test_cli.py +89 -59
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/.github/workflows/ci.yml +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/.github/workflows/publish-python-cli.yml +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/.gitignore +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/CONTRIBUTING.md +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/LICENSE +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/README.md +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/SECURITY.md +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/__init__.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_auth.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_config.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_errors.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_output.py +0 -0
- {polygres_cli-0.1.1 → polygres_cli-0.1.2}/src/polygres_cli/cli_secrets.py +0 -0
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
3
5
|
import json as jsonlib
|
|
4
6
|
import time
|
|
5
7
|
from collections.abc import Callable
|
|
6
8
|
from email.utils import parsedate_to_datetime
|
|
7
9
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
from urllib.parse import urlsplit
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import quote, urlsplit
|
|
10
12
|
|
|
11
13
|
import httpx
|
|
12
14
|
|
|
13
15
|
from polygres_cli.cli_errors import AUTH, UNAVAILABLE, CliError, api_error_from_response
|
|
14
16
|
from polygres_cli.cli_secrets import redact_string
|
|
15
17
|
|
|
16
|
-
VERSION = "0.1.
|
|
18
|
+
VERSION = "0.1.2"
|
|
17
19
|
RETRY_STATUSES = {408, 429, 500, 502, 503, 504}
|
|
18
20
|
HEAVY_REQUEST_TIMEOUT = 120.0
|
|
19
21
|
|
|
@@ -111,13 +113,121 @@ class CliControlPlaneClient:
|
|
|
111
113
|
return self._delete(f"/projects/{project_id}/api-keys/{key_id}")
|
|
112
114
|
|
|
113
115
|
def csv_preview(self, project_id: str, file: Path, fields: dict[str, str]) -> dict[str, Any]:
|
|
116
|
+
file_size = file.stat().st_size
|
|
117
|
+
session = self._post(
|
|
118
|
+
f"/projects/{project_id}/imports/csv/upload-sessions",
|
|
119
|
+
{"original_filename": file.name, "file_size_bytes": file_size},
|
|
120
|
+
)
|
|
121
|
+
upload = session.get("upload")
|
|
122
|
+
if not isinstance(upload, dict):
|
|
123
|
+
raise CliError("IMPORT_INVALID", "Upload session response is invalid.")
|
|
124
|
+
job_id = upload.get("job_id")
|
|
125
|
+
upload_url = upload.get("upload_url")
|
|
126
|
+
block_size = upload.get("block_size_bytes")
|
|
127
|
+
if (
|
|
128
|
+
not isinstance(job_id, str)
|
|
129
|
+
or not isinstance(upload_url, str)
|
|
130
|
+
or isinstance(block_size, bool)
|
|
131
|
+
or not isinstance(block_size, int)
|
|
132
|
+
or block_size <= 0
|
|
133
|
+
):
|
|
134
|
+
raise CliError("IMPORT_INVALID", "Upload session response is incomplete.")
|
|
135
|
+
sha256 = self._upload_csv_blocks(file, upload_url, block_size)
|
|
136
|
+
payload: dict[str, Any] = {
|
|
137
|
+
"job_id": job_id,
|
|
138
|
+
"original_filename": file.name,
|
|
139
|
+
"file_size_bytes": file_size,
|
|
140
|
+
"sha256": sha256,
|
|
141
|
+
"target_schema": fields.get("target_schema", "public"),
|
|
142
|
+
"target_table": fields.get("target_table"),
|
|
143
|
+
"mode": fields.get("mode", "create_table"),
|
|
144
|
+
"encoding": fields.get("encoding", "utf-8"),
|
|
145
|
+
"quote_char": fields.get("quote_char", '"'),
|
|
146
|
+
"has_header": fields.get("has_header", "true") == "true",
|
|
147
|
+
"sample_row_count": int(fields.get("sample_row_count", "50")),
|
|
148
|
+
}
|
|
149
|
+
for name in ("delimiter", "escape_char"):
|
|
150
|
+
if name in fields:
|
|
151
|
+
payload[name] = fields[name]
|
|
152
|
+
return self._post(
|
|
153
|
+
f"/projects/{project_id}/imports/csv/upload-sessions/{job_id}/complete",
|
|
154
|
+
payload,
|
|
155
|
+
timeout=HEAVY_REQUEST_TIMEOUT,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _upload_csv_blocks(self, file: Path, upload_url: str, block_size: int) -> str:
|
|
159
|
+
digest = hashlib.sha256()
|
|
160
|
+
block_ids: list[str] = []
|
|
114
161
|
with file.open("rb") as handle:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
162
|
+
index = 0
|
|
163
|
+
while chunk := handle.read(block_size):
|
|
164
|
+
digest.update(chunk)
|
|
165
|
+
block_id = base64.b64encode(f"{index:08d}".encode()).decode()
|
|
166
|
+
block_ids.append(block_id)
|
|
167
|
+
block_url = f"{upload_url}&comp=block&blockid={quote(block_id, safe='')}"
|
|
168
|
+
self._blob_request(
|
|
169
|
+
"PUT",
|
|
170
|
+
block_url,
|
|
171
|
+
content=chunk,
|
|
172
|
+
headers={
|
|
173
|
+
"Content-MD5": base64.b64encode(
|
|
174
|
+
hashlib.md5(chunk, usedforsecurity=False).digest()
|
|
175
|
+
).decode(),
|
|
176
|
+
"x-ms-version": "2023-11-03",
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
index += 1
|
|
180
|
+
if block_ids:
|
|
181
|
+
block_list = "<?xml version=\"1.0\" encoding=\"utf-8\"?><BlockList>" + "".join(
|
|
182
|
+
f"<Latest>{block_id}</Latest>" for block_id in block_ids
|
|
183
|
+
) + "</BlockList>"
|
|
184
|
+
self._blob_request(
|
|
185
|
+
"PUT",
|
|
186
|
+
f"{upload_url}&comp=blocklist",
|
|
187
|
+
content=block_list.encode(),
|
|
188
|
+
headers={"Content-Type": "application/xml", "x-ms-version": "2023-11-03"},
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
self._blob_request(
|
|
192
|
+
"PUT",
|
|
193
|
+
upload_url,
|
|
194
|
+
content=b"",
|
|
195
|
+
headers={"x-ms-blob-type": "BlockBlob", "x-ms-version": "2023-11-03"},
|
|
196
|
+
)
|
|
197
|
+
return digest.hexdigest()
|
|
198
|
+
|
|
199
|
+
def _blob_request(
|
|
200
|
+
self, method: str, url: str, *, content: bytes, headers: dict[str, str]
|
|
201
|
+
) -> None:
|
|
202
|
+
response: httpx.Response | None = None
|
|
203
|
+
for attempt in range(self._max_retries + 1):
|
|
204
|
+
try:
|
|
205
|
+
response = self._client.request(
|
|
206
|
+
method,
|
|
207
|
+
url,
|
|
208
|
+
content=content,
|
|
209
|
+
headers=headers,
|
|
210
|
+
timeout=HEAVY_REQUEST_TIMEOUT,
|
|
211
|
+
)
|
|
212
|
+
except (httpx.TimeoutException, httpx.NetworkError) as exc:
|
|
213
|
+
if attempt < self._max_retries:
|
|
214
|
+
_sleep_before_retry(attempt, None)
|
|
215
|
+
continue
|
|
216
|
+
raise CliError(
|
|
217
|
+
"IMPORT_UPLOAD_FAILED",
|
|
218
|
+
"Direct CSV upload is unavailable.",
|
|
219
|
+
exit_code=UNAVAILABLE,
|
|
220
|
+
) from exc
|
|
221
|
+
if response.status_code in RETRY_STATUSES and attempt < self._max_retries:
|
|
222
|
+
_sleep_before_retry(attempt, response.headers.get("Retry-After"))
|
|
223
|
+
continue
|
|
224
|
+
break
|
|
225
|
+
assert response is not None
|
|
226
|
+
if response.is_error:
|
|
227
|
+
raise CliError(
|
|
228
|
+
"IMPORT_UPLOAD_FAILED",
|
|
229
|
+
f"Direct CSV upload failed with HTTP {response.status_code}.",
|
|
230
|
+
exit_code=UNAVAILABLE,
|
|
121
231
|
)
|
|
122
232
|
|
|
123
233
|
def start_csv_import(
|
|
@@ -243,30 +353,12 @@ class CliControlPlaneClient:
|
|
|
243
353
|
def _delete(self, path: str) -> dict[str, Any]:
|
|
244
354
|
return self._request("DELETE", path, retry=False)
|
|
245
355
|
|
|
246
|
-
def _multipart(
|
|
247
|
-
self,
|
|
248
|
-
method: str,
|
|
249
|
-
path: str,
|
|
250
|
-
file: BinaryIO,
|
|
251
|
-
filename: str,
|
|
252
|
-
fields: dict[str, str],
|
|
253
|
-
) -> dict[str, Any]:
|
|
254
|
-
return self._request(
|
|
255
|
-
method,
|
|
256
|
-
path,
|
|
257
|
-
data=fields,
|
|
258
|
-
files={"file": (filename, file, "text/csv")},
|
|
259
|
-
retry=False,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
356
|
def _request(
|
|
263
357
|
self,
|
|
264
358
|
method: str,
|
|
265
359
|
path: str,
|
|
266
360
|
*,
|
|
267
361
|
json: dict[str, Any] | None = None,
|
|
268
|
-
data: dict[str, str] | None = None,
|
|
269
|
-
files: dict[str, Any] | None = None,
|
|
270
362
|
auth: bool = True,
|
|
271
363
|
retry: bool = False,
|
|
272
364
|
allow_refresh: bool = True,
|
|
@@ -289,8 +381,6 @@ class CliControlPlaneClient:
|
|
|
289
381
|
try:
|
|
290
382
|
request_kwargs: dict[str, Any] = {
|
|
291
383
|
"headers": headers,
|
|
292
|
-
"data": data,
|
|
293
|
-
"files": files,
|
|
294
384
|
}
|
|
295
385
|
if json is not None:
|
|
296
386
|
headers["Content-Type"] = "application/json"
|
|
@@ -334,8 +424,6 @@ class CliControlPlaneClient:
|
|
|
334
424
|
method,
|
|
335
425
|
path,
|
|
336
426
|
json=json,
|
|
337
|
-
data=data,
|
|
338
|
-
files=files,
|
|
339
427
|
auth=auth,
|
|
340
428
|
retry=retry,
|
|
341
429
|
allow_refresh=False,
|
|
@@ -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.2"
|
|
35
35
|
|
|
36
36
|
whoami = subprocess.run(
|
|
37
37
|
[executable, "--json", "whoami"],
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
6
|
+
import re
|
|
5
7
|
import stat
|
|
6
8
|
import subprocess
|
|
7
9
|
from datetime import datetime, timedelta, timezone
|
|
@@ -36,6 +38,46 @@ def _stub(route: object, **kwargs: object) -> object:
|
|
|
36
38
|
return getattr(route, "mo" + "ck")(**kwargs)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _stub_csv_direct_preview(
|
|
42
|
+
preview: dict[str, object], *, status_code: int = 200
|
|
43
|
+
) -> tuple[object, object, object, object]:
|
|
44
|
+
session_route = _stub(
|
|
45
|
+
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv/upload-sessions"),
|
|
46
|
+
return_value=httpx.Response(
|
|
47
|
+
status_code if status_code != 200 else 200,
|
|
48
|
+
json=(
|
|
49
|
+
preview
|
|
50
|
+
if status_code != 200
|
|
51
|
+
else {
|
|
52
|
+
"upload": {
|
|
53
|
+
"job_id": IMPORT_ID,
|
|
54
|
+
"upload_url": "https://blob.example.test/file.csv?sig=secret",
|
|
55
|
+
"block_size_bytes": 8 * 1024 * 1024,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
block_route = _stub(
|
|
62
|
+
respx.put(
|
|
63
|
+
re.compile(r"https://blob\.example\.test/file\.csv\?.*comp=block&blockid=.*")
|
|
64
|
+
),
|
|
65
|
+
return_value=httpx.Response(201),
|
|
66
|
+
)
|
|
67
|
+
commit_route = _stub(
|
|
68
|
+
respx.put(re.compile(r"https://blob\.example\.test/file\.csv\?.*comp=blocklist.*")),
|
|
69
|
+
return_value=httpx.Response(201),
|
|
70
|
+
)
|
|
71
|
+
complete_route = _stub(
|
|
72
|
+
respx.post(
|
|
73
|
+
f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv/upload-sessions/"
|
|
74
|
+
f"{IMPORT_ID}/complete"
|
|
75
|
+
),
|
|
76
|
+
return_value=httpx.Response(200, json=preview),
|
|
77
|
+
)
|
|
78
|
+
return session_route, block_route, commit_route, complete_route
|
|
79
|
+
|
|
80
|
+
|
|
39
81
|
def run_cli(
|
|
40
82
|
args: list[str],
|
|
41
83
|
capsys: pytest.CaptureFixture[str],
|
|
@@ -93,7 +135,7 @@ def test_version_and_config_path_json(
|
|
|
93
135
|
) -> None:
|
|
94
136
|
rc, out, _ = run_cli(["--version"], capsys, monkeypatch, tmp_path)
|
|
95
137
|
assert rc == 0
|
|
96
|
-
assert out.strip() == "polygres 0.1.
|
|
138
|
+
assert out.strip() == "polygres 0.1.2"
|
|
97
139
|
|
|
98
140
|
rc, out, _ = run_cli(["--json", "config", "path"], capsys, monkeypatch, tmp_path)
|
|
99
141
|
assert rc == 0
|
|
@@ -430,7 +472,7 @@ def test_projects_list_uses_env_token_and_selected_project_json(
|
|
|
430
472
|
assert err == ""
|
|
431
473
|
assert route.called
|
|
432
474
|
assert route.calls[0].request.headers["Authorization"] == f"Bearer {ACCESS_TOKEN}"
|
|
433
|
-
assert route.calls[0].request.headers["User-Agent"] == "polygres-cli/0.1.
|
|
475
|
+
assert route.calls[0].request.headers["User-Agent"] == "polygres-cli/0.1.2"
|
|
434
476
|
assert json.loads(out) == {
|
|
435
477
|
"projects": [{"id": PROJECT_ID, "name": "Support", "status": "ready"}],
|
|
436
478
|
"selected_project_id": PROJECT_ID,
|
|
@@ -1066,18 +1108,14 @@ def test_import_csv_sends_sample_row_count_only_to_preview(
|
|
|
1066
1108
|
write_config(tmp_path, {"version": 1, "selected_project_id": PROJECT_ID})
|
|
1067
1109
|
csv_path = tmp_path / "documents.csv"
|
|
1068
1110
|
csv_path.write_text("id,title\n1,Hello\n", encoding="utf-8")
|
|
1069
|
-
preview_route =
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
"
|
|
1075
|
-
"preview": {
|
|
1076
|
-
"job_id": IMPORT_ID,
|
|
1077
|
-
"columns": [{"name": "id"}, {"name": "title"}],
|
|
1078
|
-
},
|
|
1111
|
+
session_route, block_route, commit_route, preview_route = _stub_csv_direct_preview(
|
|
1112
|
+
{
|
|
1113
|
+
"request_id": "req_preview",
|
|
1114
|
+
"preview": {
|
|
1115
|
+
"job_id": IMPORT_ID,
|
|
1116
|
+
"columns": [{"name": "id"}, {"name": "title"}],
|
|
1079
1117
|
},
|
|
1080
|
-
|
|
1118
|
+
}
|
|
1081
1119
|
)
|
|
1082
1120
|
import_route = _stub(
|
|
1083
1121
|
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"),
|
|
@@ -1110,9 +1148,15 @@ def test_import_csv_sends_sample_row_count_only_to_preview(
|
|
|
1110
1148
|
assert err == ""
|
|
1111
1149
|
assert preview_route.called
|
|
1112
1150
|
assert import_route.called
|
|
1113
|
-
|
|
1151
|
+
complete_payload = json.loads(preview_route.calls[0].request.content)
|
|
1114
1152
|
import_payload = json.loads(import_route.calls[0].request.content)
|
|
1115
|
-
assert
|
|
1153
|
+
assert session_route.called
|
|
1154
|
+
assert block_route.called
|
|
1155
|
+
assert commit_route.called
|
|
1156
|
+
assert complete_payload["sample_row_count"] == 50
|
|
1157
|
+
assert complete_payload["file_size_bytes"] == csv_path.stat().st_size
|
|
1158
|
+
assert complete_payload["sha256"] == hashlib.sha256(csv_path.read_bytes()).hexdigest()
|
|
1159
|
+
assert csv_path.read_bytes() not in preview_route.calls[0].request.content
|
|
1116
1160
|
assert import_route.calls[0].request.headers["content-type"] == "application/json"
|
|
1117
1161
|
assert import_payload["job_id"] == IMPORT_ID
|
|
1118
1162
|
assert [column["name"] for column in import_payload["columns"]] == ["id", "title"]
|
|
@@ -1129,19 +1173,16 @@ def test_import_csv_preserves_tier_limit_error_and_skips_final_submission(
|
|
|
1129
1173
|
write_config(tmp_path, {"version": 1, "selected_project_id": PROJECT_ID})
|
|
1130
1174
|
csv_path = tmp_path / "documents.csv"
|
|
1131
1175
|
csv_path.write_text("id,title\n1,Hello\n", encoding="utf-8")
|
|
1132
|
-
preview_route =
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
"
|
|
1138
|
-
"
|
|
1139
|
-
"code": "IMPORT_LIMIT_EXCEEDED",
|
|
1140
|
-
"message": "File exceeds the project tier storage limit.",
|
|
1141
|
-
"details": {"limit_bytes": 8, "source": "storage_bytes"},
|
|
1142
|
-
},
|
|
1176
|
+
preview_route, _, _, _ = _stub_csv_direct_preview(
|
|
1177
|
+
{
|
|
1178
|
+
"request_id": "req_import_limit",
|
|
1179
|
+
"error": {
|
|
1180
|
+
"code": "IMPORT_LIMIT_EXCEEDED",
|
|
1181
|
+
"message": "File exceeds the project tier storage limit.",
|
|
1182
|
+
"details": {"limit_bytes": 8, "source": "storage_bytes"},
|
|
1143
1183
|
},
|
|
1144
|
-
|
|
1184
|
+
},
|
|
1185
|
+
status_code=413,
|
|
1145
1186
|
)
|
|
1146
1187
|
import_route = _stub(respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"))
|
|
1147
1188
|
|
|
@@ -1605,16 +1646,12 @@ def test_csv_import_uses_preview_columns_and_wait_polling(
|
|
|
1605
1646
|
csv_path = tmp_path / "documents.csv"
|
|
1606
1647
|
csv_path.write_text("id,title\n1,Hello\n", encoding="utf-8")
|
|
1607
1648
|
columns = [{"name": "id", "type": "text", "nullable": False}]
|
|
1608
|
-
preview_route =
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
"preview": {"job_id": IMPORT_ID, "columns": columns},
|
|
1615
|
-
},
|
|
1616
|
-
),
|
|
1617
|
-
)
|
|
1649
|
+
preview_route = _stub_csv_direct_preview(
|
|
1650
|
+
{
|
|
1651
|
+
"request_id": "req_preview",
|
|
1652
|
+
"preview": {"job_id": IMPORT_ID, "columns": columns},
|
|
1653
|
+
}
|
|
1654
|
+
)[3]
|
|
1618
1655
|
import_route = _stub(
|
|
1619
1656
|
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"),
|
|
1620
1657
|
return_value=httpx.Response(
|
|
@@ -1846,7 +1883,7 @@ def test_login_browser_fallback_polls_stores_tokens_and_redacts_output(
|
|
|
1846
1883
|
assert ACCESS_TOKEN not in out
|
|
1847
1884
|
assert REFRESH_TOKEN not in out
|
|
1848
1885
|
assert json.loads(start_route.calls[0].request.content) == {
|
|
1849
|
-
"client": {"name": "polygres-cli", "version": "0.1.
|
|
1886
|
+
"client": {"name": "polygres-cli", "version": "0.1.2"}
|
|
1850
1887
|
}
|
|
1851
1888
|
assert json.loads(poll_route.calls[0].request.content) == {
|
|
1852
1889
|
"login_session_id": "cls_abcdefghijklmnopqrstuvwxyz",
|
|
@@ -1914,22 +1951,18 @@ def test_csv_import_propagates_preview_effective_parser_settings(
|
|
|
1914
1951
|
path = tmp_path / "documents.csv"
|
|
1915
1952
|
path.write_text("id|title\n1|Hello\n", encoding="utf-8")
|
|
1916
1953
|
columns = [{"name": "id", "type": "text", "nullable": False}]
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
"
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
"columns": columns,
|
|
1930
|
-
}
|
|
1931
|
-
},
|
|
1932
|
-
),
|
|
1954
|
+
_stub_csv_direct_preview(
|
|
1955
|
+
{
|
|
1956
|
+
"preview": {
|
|
1957
|
+
"job_id": IMPORT_ID,
|
|
1958
|
+
"encoding": "utf-8-sig",
|
|
1959
|
+
"delimiter": "|",
|
|
1960
|
+
"quote_char": "'",
|
|
1961
|
+
"escape_char": "\\",
|
|
1962
|
+
"has_header": False,
|
|
1963
|
+
"columns": columns,
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1933
1966
|
)
|
|
1934
1967
|
import_route = _stub(
|
|
1935
1968
|
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"),
|
|
@@ -2084,11 +2117,8 @@ def test_human_import_polling_writes_progress_only_to_stderr(
|
|
|
2084
2117
|
path = tmp_path / "documents.csv"
|
|
2085
2118
|
path.write_text("id\n1\n", encoding="utf-8")
|
|
2086
2119
|
monkeypatch.setattr(cli.time, "sleep", lambda _seconds: None)
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
return_value=httpx.Response(
|
|
2090
|
-
200, json={"preview": {"job_id": IMPORT_ID, "columns": [{"name": "id"}]}}
|
|
2091
|
-
),
|
|
2120
|
+
_stub_csv_direct_preview(
|
|
2121
|
+
{"preview": {"job_id": IMPORT_ID, "columns": [{"name": "id"}]}}
|
|
2092
2122
|
)
|
|
2093
2123
|
_stub(
|
|
2094
2124
|
respx.post(f"{API_BASE_URL}/projects/{PROJECT_ID}/imports/csv"),
|
|
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
|
|
File without changes
|
|
File without changes
|