snowflake-sql-api 0.1.4__tar.gz → 0.1.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.
Files changed (48) hide show
  1. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/PKG-INFO +29 -1
  2. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/README.md +28 -0
  3. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/_version.py +2 -2
  4. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/aclient.py +58 -9
  5. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/client.py +69 -8
  6. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/exceptions.py +2 -0
  7. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/retry.py +5 -5
  8. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/transport.py +12 -5
  9. snowflake_sql_api-0.1.5/tests/smoke/test_submissions.py +300 -0
  10. snowflake_sql_api-0.1.5/tests/test_submissions.py +519 -0
  11. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/.gitignore +0 -0
  12. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/CONTRIBUTING.md +0 -0
  13. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/LICENSE +0 -0
  14. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/pyproject.toml +0 -0
  15. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/__init__.py +0 -0
  16. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/auth.py +0 -0
  17. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/batches.py +0 -0
  18. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/bindings.py +0 -0
  19. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/cli.py +0 -0
  20. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/escaping.py +0 -0
  21. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/exports.py +0 -0
  22. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/pagination.py +0 -0
  23. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/py.typed +0 -0
  24. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/row_mapping.py +0 -0
  25. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/testing.py +0 -0
  26. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/snowflake_sql_api/types.py +0 -0
  27. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/__init__.py +0 -0
  28. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/conftest.py +0 -0
  29. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/smoke/__init__.py +0 -0
  30. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/smoke/test_smoke.py +0 -0
  31. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/streaming_support.py +0 -0
  32. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/support.py +0 -0
  33. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_aclient.py +0 -0
  34. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_auth.py +0 -0
  35. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_batch_writes.py +0 -0
  36. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_bindings.py +0 -0
  37. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_cli.py +0 -0
  38. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_client.py +0 -0
  39. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_escaping.py +0 -0
  40. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_exports.py +0 -0
  41. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_import_footprint.py +0 -0
  42. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_package.py +0 -0
  43. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_regression.py +0 -0
  44. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_retry.py +0 -0
  45. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_streaming.py +0 -0
  46. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_testing.py +0 -0
  47. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_transport.py +0 -0
  48. {snowflake_sql_api-0.1.4 → snowflake_sql_api-0.1.5}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: snowflake-sql-api
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Lightweight, pure-Python client for Snowflake's SQL API v2
5
5
  Project-URL: Homepage, https://github.com/hampsterx/snowflake-sql-api
6
6
  Project-URL: Repository, https://github.com/hampsterx/snowflake-sql-api
@@ -95,6 +95,7 @@ stays small and cold-starts quickly. Core dependencies: `httpx`, `PyJWT`,
95
95
  - Multi-partition result handling for large result sets
96
96
  - Lazy row streams and bounded row batches, with CSV and JSONL exports
97
97
  - Query helpers, DML, batch insert, async statement submission
98
+ - Caller-owned submission UUIDs and explicit replay across worker restarts
98
99
  - A small CLI for ad-hoc queries
99
100
  - Optional pandas (`[pandas]`) and typed-row (`[pydantic]`) helpers, kept out of
100
101
  the default install
@@ -174,6 +175,33 @@ do not resume with the same advanced iterator. The async equivalent is awaited a
174
175
  accepts the same synchronous iterable, whose production can block the event loop.
175
176
  See [batch writes](docs/batch-writes.md) for limits and recovery semantics.
176
177
 
178
+ ## Replayable submissions
179
+
180
+ For a queued job, allocate a UUID and persist it with the SQL, typed inputs and
181
+ explicit execution context **before** calling `submit()`. Pass that saved ID:
182
+
183
+ ```python
184
+ handle = client.submit(job["sql"], job["params"], request_id=job["request_id"])
185
+ # Persist handle.statement_handle when it reaches the worker.
186
+
187
+ # After a restart with no saved statement handle, rebuild identical inputs:
188
+ handle = client.resubmit(job["sql"], job["params"], request_id=job["request_id"])
189
+ rows = handle.result() # Only then mark the durable job complete.
190
+ ```
191
+
192
+ `resubmit()` sends the same ID with `retry=true` on its first HTTP attempt.
193
+ A saved statement handle can instead be attached with `QueryHandle(client, saved_handle)`
194
+ and polled without another submission. Both clients expose `handle.request_id`, and
195
+ submission errors in the `SnowflakeError` family carry the allocated ID. Async
196
+ submission, resubmission and result methods are awaited.
197
+
198
+ The application owns durable storage, identical payload reconstruction, UUID
199
+ uniqueness and reconciliation. Retained Snowflake history governs recovery; there
200
+ is no unlimited replay window or exactly-once processing guarantee. Live restart
201
+ verification requires the opt-in disposable-resource probes. See
202
+ [replayable submissions](docs/submissions.md) for the complete persistence recipe,
203
+ limits and verification status.
204
+
177
205
  ## Prior art and alternatives
178
206
 
179
207
  `snowflake-sql-api` is not the first REST-based Snowflake client. If it does not
@@ -31,6 +31,7 @@ stays small and cold-starts quickly. Core dependencies: `httpx`, `PyJWT`,
31
31
  - Multi-partition result handling for large result sets
32
32
  - Lazy row streams and bounded row batches, with CSV and JSONL exports
33
33
  - Query helpers, DML, batch insert, async statement submission
34
+ - Caller-owned submission UUIDs and explicit replay across worker restarts
34
35
  - A small CLI for ad-hoc queries
35
36
  - Optional pandas (`[pandas]`) and typed-row (`[pydantic]`) helpers, kept out of
36
37
  the default install
@@ -110,6 +111,33 @@ do not resume with the same advanced iterator. The async equivalent is awaited a
110
111
  accepts the same synchronous iterable, whose production can block the event loop.
111
112
  See [batch writes](docs/batch-writes.md) for limits and recovery semantics.
112
113
 
114
+ ## Replayable submissions
115
+
116
+ For a queued job, allocate a UUID and persist it with the SQL, typed inputs and
117
+ explicit execution context **before** calling `submit()`. Pass that saved ID:
118
+
119
+ ```python
120
+ handle = client.submit(job["sql"], job["params"], request_id=job["request_id"])
121
+ # Persist handle.statement_handle when it reaches the worker.
122
+
123
+ # After a restart with no saved statement handle, rebuild identical inputs:
124
+ handle = client.resubmit(job["sql"], job["params"], request_id=job["request_id"])
125
+ rows = handle.result() # Only then mark the durable job complete.
126
+ ```
127
+
128
+ `resubmit()` sends the same ID with `retry=true` on its first HTTP attempt.
129
+ A saved statement handle can instead be attached with `QueryHandle(client, saved_handle)`
130
+ and polled without another submission. Both clients expose `handle.request_id`, and
131
+ submission errors in the `SnowflakeError` family carry the allocated ID. Async
132
+ submission, resubmission and result methods are awaited.
133
+
134
+ The application owns durable storage, identical payload reconstruction, UUID
135
+ uniqueness and reconciliation. Retained Snowflake history governs recovery; there
136
+ is no unlimited replay window or exactly-once processing guarantee. Live restart
137
+ verification requires the opt-in disposable-resource probes. See
138
+ [replayable submissions](docs/submissions.md) for the complete persistence recipe,
139
+ limits and verification status.
140
+
113
141
  ## Prior art and alternatives
114
142
 
115
143
  `snowflake-sql-api` is not the first REST-based Snowflake client. If it does not
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.4'
22
- __version_tuple__ = version_tuple = (0, 1, 4)
21
+ __version__ = version = '0.1.5'
22
+ __version_tuple__ = version_tuple = (0, 1, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -30,11 +30,12 @@ import httpx
30
30
  from .auth import KeypairAuthenticator, account_hostname
31
31
  from .batches import BatchWriteResult, _BatchExecution, _BatchPlanner
32
32
  from .bindings import to_bindings
33
- from .client import DEFAULT_POLL_TIMEOUT, _columns_from, _rows_affected
33
+ from .client import DEFAULT_POLL_TIMEOUT, _columns_from, _rows_affected, _submission_id
34
34
  from .escaping import quote_identifier, quote_name
35
35
  from .exceptions import (
36
36
  ResultNotReady,
37
37
  SnowflakeConfigError,
38
+ SnowflakeError,
38
39
  SnowflakeRequestError,
39
40
  SnowflakeTimeoutError,
40
41
  )
@@ -61,9 +62,16 @@ OnQuery = Callable[[str, "Params"], None]
61
62
  class AsyncQueryHandle:
62
63
  """Async handle to a submitted statement (see :class:`~snowflake_sql_api.client.QueryHandle`)."""
63
64
 
64
- def __init__(self, client: AsyncSnowflakeClient, statement_handle: str) -> None:
65
+ def __init__(
66
+ self,
67
+ client: AsyncSnowflakeClient,
68
+ statement_handle: str,
69
+ *,
70
+ request_id: Optional[str] = None,
71
+ ) -> None:
65
72
  self._client = client
66
73
  self.statement_handle = statement_handle
74
+ self.request_id = request_id
67
75
 
68
76
  async def result(
69
77
  self, *, poll: bool = True, timeout: Optional[float] = None
@@ -436,13 +444,54 @@ class AsyncSnowflakeClient:
436
444
  del batch
437
445
  return planner.result()
438
446
 
439
- async def submit(self, sql: str, params: Params = None) -> AsyncQueryHandle:
440
- """Submit a long-running statement and return an :class:`AsyncQueryHandle`."""
441
- response = await self._run(sql, params, async_exec=True)
442
- handle = response.statement_handle
443
- if handle is None:
444
- raise SnowflakeRequestError("async submit returned no statement handle")
445
- return AsyncQueryHandle(self, handle)
447
+ async def submit(
448
+ self,
449
+ sql: str,
450
+ params: Params = None,
451
+ *,
452
+ request_id: str | uuid.UUID | None = None,
453
+ ) -> AsyncQueryHandle:
454
+ """Submit once with a caller-owned UUID, or generate a fresh UUID4.
455
+
456
+ Persist the ID and identical inputs before calling for restart recovery.
457
+ Use :meth:`resubmit` once a previous attempt might have reached Snowflake.
458
+ """
459
+ return await self._submit_handle(
460
+ sql, params, request_id=request_id, retry=False
461
+ )
462
+
463
+ async def resubmit(
464
+ self, sql: str, params: Params = None, *, request_id: str | uuid.UUID
465
+ ) -> AsyncQueryHandle:
466
+ """Replay identical saved inputs with the same UUID and ``retry=true``.
467
+
468
+ The caller owns durable storage, payload consistency and reconciliation
469
+ when Snowflake's retained history cannot resolve an uncertain outcome.
470
+ """
471
+ return await self._submit_handle(sql, params, request_id=request_id, retry=True)
472
+
473
+ async def _submit_handle(
474
+ self,
475
+ sql: str,
476
+ params: Params,
477
+ *,
478
+ request_id: str | uuid.UUID | None,
479
+ retry: bool,
480
+ ) -> AsyncQueryHandle:
481
+ canonical_id = _submission_id(request_id, retry=retry)
482
+ self._notify(sql, params)
483
+ try:
484
+ payload = self._build_payload(sql, params)
485
+ response = await self._transport.submit(
486
+ payload, request_id=canonical_id, async_exec=True, retry=retry
487
+ )
488
+ handle = response.statement_handle
489
+ if handle is None:
490
+ raise SnowflakeRequestError("async submit returned no statement handle")
491
+ return AsyncQueryHandle(self, handle, request_id=canonical_id)
492
+ except SnowflakeError as exc:
493
+ exc.request_id = canonical_id
494
+ raise
446
495
 
447
496
  # -- lifecycle --------------------------------------------------------
448
497
 
@@ -35,6 +35,7 @@ from .escaping import quote_identifier, quote_name
35
35
  from .exceptions import (
36
36
  ResultNotReady,
37
37
  SnowflakeConfigError,
38
+ SnowflakeError,
38
39
  SnowflakeRequestError,
39
40
  SnowflakeTimeoutError,
40
41
  )
@@ -61,6 +62,20 @@ OnQuery = Callable[[str, "Params"], None]
61
62
  DEFAULT_POLL_TIMEOUT = 600.0
62
63
 
63
64
 
65
+ def _submission_id(request_id: str | uuid.UUID | None, *, retry: bool) -> str:
66
+ """Allocate or canonicalize an identity before hooks, binding or HTTP."""
67
+ if request_id is None and not retry:
68
+ return str(uuid.uuid4())
69
+ if isinstance(request_id, uuid.UUID):
70
+ return str(request_id)
71
+ if not isinstance(request_id, str):
72
+ raise SnowflakeConfigError("request_id must be a UUID string or UUID object")
73
+ try:
74
+ return str(uuid.UUID(request_id))
75
+ except ValueError as exc:
76
+ raise SnowflakeConfigError("request_id must be a valid UUID") from exc
77
+
78
+
64
79
  def _affected_count(value: Any, strict: bool) -> int:
65
80
  if strict and (isinstance(value, bool) or not isinstance(value, (int, str))):
66
81
  raise ValueError("rows-affected count must be an integer")
@@ -108,9 +123,16 @@ class QueryHandle:
108
123
  (regression bug #3).
109
124
  """
110
125
 
111
- def __init__(self, client: SnowflakeClient, statement_handle: str) -> None:
126
+ def __init__(
127
+ self,
128
+ client: SnowflakeClient,
129
+ statement_handle: str,
130
+ *,
131
+ request_id: Optional[str] = None,
132
+ ) -> None:
112
133
  self._client = client
113
134
  self.statement_handle = statement_handle
135
+ self.request_id = request_id
114
136
 
115
137
  def result(
116
138
  self, *, poll: bool = True, timeout: Optional[float] = None
@@ -483,13 +505,52 @@ class SnowflakeClient:
483
505
 
484
506
  # -- async submit -----------------------------------------------------
485
507
 
486
- def submit(self, sql: str, params: Params = None) -> QueryHandle:
487
- """Submit a long-running statement and return a :class:`QueryHandle`."""
488
- response = self._run(sql, params, async_exec=True)
489
- handle = response.statement_handle
490
- if handle is None:
491
- raise SnowflakeRequestError("async submit returned no statement handle")
492
- return QueryHandle(self, handle)
508
+ def submit(
509
+ self,
510
+ sql: str,
511
+ params: Params = None,
512
+ *,
513
+ request_id: str | uuid.UUID | None = None,
514
+ ) -> QueryHandle:
515
+ """Submit once with a caller-owned UUID, or generate a fresh UUID4.
516
+
517
+ Persist the ID and identical inputs before calling for restart recovery.
518
+ Use :meth:`resubmit` once a previous attempt might have reached Snowflake.
519
+ """
520
+ return self._submit_handle(sql, params, request_id=request_id, retry=False)
521
+
522
+ def resubmit(
523
+ self, sql: str, params: Params = None, *, request_id: str | uuid.UUID
524
+ ) -> QueryHandle:
525
+ """Replay identical saved inputs with the same UUID and ``retry=true``.
526
+
527
+ The caller owns durable storage, payload consistency and reconciliation
528
+ when Snowflake's retained history cannot resolve an uncertain outcome.
529
+ """
530
+ return self._submit_handle(sql, params, request_id=request_id, retry=True)
531
+
532
+ def _submit_handle(
533
+ self,
534
+ sql: str,
535
+ params: Params,
536
+ *,
537
+ request_id: str | uuid.UUID | None,
538
+ retry: bool,
539
+ ) -> QueryHandle:
540
+ canonical_id = _submission_id(request_id, retry=retry)
541
+ self._notify(sql, params)
542
+ try:
543
+ payload = self._build_payload(sql, params)
544
+ response = self._transport.submit(
545
+ payload, request_id=canonical_id, async_exec=True, retry=retry
546
+ )
547
+ handle = response.statement_handle
548
+ if handle is None:
549
+ raise SnowflakeRequestError("async submit returned no statement handle")
550
+ return QueryHandle(self, handle, request_id=canonical_id)
551
+ except SnowflakeError as exc:
552
+ exc.request_id = canonical_id
553
+ raise
493
554
 
494
555
  # -- lifecycle --------------------------------------------------------
495
556
 
@@ -34,6 +34,8 @@ class SnowflakeError(Exception):
34
34
  errors outside statement submission (including GET requests).
35
35
  """
36
36
 
37
+ request_id: Optional[str] = None
38
+
37
39
  def __init__(
38
40
  self, *args: object, submission_attempts: Optional[int] = None
39
41
  ) -> None:
@@ -2,10 +2,10 @@
2
2
 
3
3
  Defines which conditions are retryable (connect/read timeout, HTTP 429, 5xx,
4
4
  status-poll, partition fetch) and the backoff schedule. Retries reuse the
5
- statement ``requestId`` so a retried DML submit cannot double-apply
6
- (idempotency); the transport owns that wiring.
7
-
8
- Scaffold only: the retry decision and backoff loop land in Phase 2.
5
+ statement ``requestId`` and add ``retry=true`` after the initial attempt.
6
+ Snowflake uses retained statement history to resolve an identical submission;
7
+ this is not an unlimited deduplication or exactly-once guarantee. The transport
8
+ owns this wiring.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -32,7 +32,7 @@ class RetryPolicy:
32
32
  multiplier: float = 2.0
33
33
  jitter: bool = True
34
34
  retry_statuses: FrozenSet[int] = field(
35
- # 408 (request timeout) is safe to retry: submits reuse their requestId.
35
+ # Submissions retain their requestId and use retry=true after a timeout.
36
36
  default_factory=lambda: frozenset({408, 429, 500, 502, 503, 504})
37
37
  )
38
38
 
@@ -4,8 +4,9 @@ Owns the actual ``httpx`` calls (sync and async), status-code interpretation
4
4
  (200 success, 202 long-running/async, 422 execution failure, 401 auth,
5
5
  403 forbidden, 429/5xx retryable), the retry loop, and ``requestId`` reuse for
6
6
  idempotent retries. A retried submit re-POSTs the identical body with the same
7
- ``requestId`` and ``retry=true`` so Snowflake deduplicates instead of
8
- double-applying DML.
7
+ ``requestId`` and ``retry=true``. Snowflake resolves identical submissions
8
+ through retained statement history, with no unlimited replay window or
9
+ application-wide exactly-once guarantee.
9
10
 
10
11
  Higher layers (``client``/``aclient``) talk to the transport, not to ``httpx``.
11
12
  """
@@ -156,8 +157,12 @@ class _TransportBase:
156
157
  "User-Agent": self._user_agent,
157
158
  }
158
159
 
159
- def _submit_params(self, request_id: str, async_exec: bool) -> Dict[str, str]:
160
+ def _submit_params(
161
+ self, request_id: str, async_exec: bool, retry: bool = False
162
+ ) -> Dict[str, str]:
160
163
  params = {"requestId": request_id}
164
+ if retry:
165
+ params["retry"] = "true"
161
166
  if async_exec:
162
167
  params["async"] = "true"
163
168
  return params
@@ -196,13 +201,14 @@ class Transport(_TransportBase):
196
201
  *,
197
202
  request_id: str,
198
203
  async_exec: bool = False,
204
+ retry: bool = False,
199
205
  content: Optional[bytes] = None,
200
206
  ) -> StatementResponse:
201
207
  """POST a statement, retrying idempotently on transient failure."""
202
208
  return self._send(
203
209
  "POST",
204
210
  STATEMENTS_PATH,
205
- params=self._submit_params(request_id, async_exec),
211
+ params=self._submit_params(request_id, async_exec, retry),
206
212
  json=payload if content is None else None,
207
213
  content=content,
208
214
  is_submit=True,
@@ -308,12 +314,13 @@ class AsyncTransport(_TransportBase):
308
314
  *,
309
315
  request_id: str,
310
316
  async_exec: bool = False,
317
+ retry: bool = False,
311
318
  content: Optional[bytes] = None,
312
319
  ) -> StatementResponse:
313
320
  return await self._send(
314
321
  "POST",
315
322
  STATEMENTS_PATH,
316
- params=self._submit_params(request_id, async_exec),
323
+ params=self._submit_params(request_id, async_exec, retry),
317
324
  json=payload if content is None else None,
318
325
  content=content,
319
326
  is_submit=True,