diffio 0.1.49__tar.gz → 0.1.51__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: diffio
3
- Version: 0.1.49
3
+ Version: 0.1.51
4
4
  Summary: Python SDK for the Diffio API.
5
5
  Author: Diffio
6
6
  License: MIT
@@ -58,6 +58,14 @@ export DIFFIO_API_BASE_URL="https://api.diffio.ai/v1"
58
58
  Use request options to override headers, timeouts, retries, or the API key per request.
59
59
  You can also pass `timeoutInSeconds` as an alias for `timeout`.
60
60
 
61
+ Generation creation is retried only when you supply a non-empty `idempotencyKey`,
62
+ even when `maxRetries` is configured globally or per request. Without a key, a
63
+ timeout or lost response can hide an accepted generation, so the SDK returns the
64
+ error after the first attempt. With a key, retries send the same key and payload.
65
+ Reuse that key when manually retrying the same operation; use a new key for a new
66
+ generation. The audio isolation and restore helpers do not supply a key and do
67
+ not automatically retry their generation-creation step.
68
+
61
69
  ```py
62
70
  from diffio import DiffioClient, RequestOptions
63
71
 
@@ -90,6 +98,7 @@ generation = client.create_generation(
90
98
  model="diffio-3.5",
91
99
  sampling={"steps": 12, "guidance": 1.5},
92
100
  idempotencyKey="restore-job-2026-001",
101
+ requestOptions={"maxRetries": 2},
93
102
  )
94
103
 
95
104
  print(generation.generationId)
@@ -141,6 +150,11 @@ print(info["apiProjectId"], info["generationId"])
141
150
 
142
151
  ## Generation progress
143
152
 
153
+ `wait_for_generation` and `generations.wait_for_complete` wait for the overall
154
+ `status` to become `complete`. Individual stages can reach 100% while video
155
+ restoration or final settlement is still pending; stage progress alone does not
156
+ indicate overall completion.
157
+
144
158
  ```py
145
159
  from diffio import DiffioClient
146
160
 
@@ -30,6 +30,14 @@ export DIFFIO_API_BASE_URL="https://api.diffio.ai/v1"
30
30
  Use request options to override headers, timeouts, retries, or the API key per request.
31
31
  You can also pass `timeoutInSeconds` as an alias for `timeout`.
32
32
 
33
+ Generation creation is retried only when you supply a non-empty `idempotencyKey`,
34
+ even when `maxRetries` is configured globally or per request. Without a key, a
35
+ timeout or lost response can hide an accepted generation, so the SDK returns the
36
+ error after the first attempt. With a key, retries send the same key and payload.
37
+ Reuse that key when manually retrying the same operation; use a new key for a new
38
+ generation. The audio isolation and restore helpers do not supply a key and do
39
+ not automatically retry their generation-creation step.
40
+
33
41
  ```py
34
42
  from diffio import DiffioClient, RequestOptions
35
43
 
@@ -62,6 +70,7 @@ generation = client.create_generation(
62
70
  model="diffio-3.5",
63
71
  sampling={"steps": 12, "guidance": 1.5},
64
72
  idempotencyKey="restore-job-2026-001",
73
+ requestOptions={"maxRetries": 2},
65
74
  )
66
75
 
67
76
  print(generation.generationId)
@@ -113,6 +122,11 @@ print(info["apiProjectId"], info["generationId"])
113
122
 
114
123
  ## Generation progress
115
124
 
125
+ `wait_for_generation` and `generations.wait_for_complete` wait for the overall
126
+ `status` to become `complete`. Individual stages can reach 100% while video
127
+ restoration or final settlement is still pending; stage progress alone does not
128
+ indicate overall completion.
129
+
116
130
  ```py
117
131
  from diffio import DiffioClient
118
132
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffio"
7
- version = "0.1.49"
7
+ version = "0.1.51"
8
8
  description = "Python SDK for the Diffio API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -70,4 +70,4 @@ __all__ = [
70
70
  "WebhooksClient",
71
71
  ]
72
72
 
73
- __version__ = "0.1.49"
73
+ __version__ = "0.1.51"
@@ -311,6 +311,7 @@ class DiffioClient:
311
311
  idempotencyKey=None,
312
312
  requestOptions=None,
313
313
  ):
314
+ """Create a generation; automatic retries require a supplied idempotency key."""
314
315
  endpoint = MODEL_ENDPOINTS.get(model)
315
316
  if not endpoint:
316
317
  raise ValueError(f"Unsupported model: {model}")
@@ -323,7 +324,13 @@ class DiffioClient:
323
324
  if idempotencyKey is not None:
324
325
  payload["idempotencyKey"] = idempotencyKey
325
326
 
326
- response = self._request("POST", endpoint, json_payload=payload, requestOptions=requestOptions)
327
+ response = self._request(
328
+ "POST",
329
+ endpoint,
330
+ json_payload=payload,
331
+ requestOptions=requestOptions,
332
+ allow_retries=isinstance(idempotencyKey, str) and bool(idempotencyKey.strip()),
333
+ )
327
334
  return CreateGenerationResponse.from_dict(response)
328
335
 
329
336
  def list_projects(self, *, requestOptions=None):
@@ -734,7 +741,7 @@ class DiffioClient:
734
741
  raiseOnError=raiseOnError,
735
742
  )
736
743
 
737
- def _request(self, method, path, *, json_payload, requestOptions=None):
744
+ def _request(self, method, path, *, json_payload, requestOptions=None, allow_retries=True):
738
745
  request_path = path.lstrip("/")
739
746
  if self._api_prefix:
740
747
  request_path = f"{self._api_prefix}/{request_path}"
@@ -743,6 +750,9 @@ class DiffioClient:
743
750
  headers = _merge_headers({"Authorization": f"Bearer {api_key}"}, merged_options.headers)
744
751
  timeout = merged_options.timeout
745
752
  max_retries = merged_options.maxRetries if merged_options.maxRetries is not None else 0
753
+ # A lost response can hide an accepted generation; only its stable key makes replay safe.
754
+ if not allow_retries:
755
+ max_retries = 0
746
756
  retry_backoff = (
747
757
  merged_options.retryBackoff if merged_options.retryBackoff is not None else DEFAULT_RETRY_BACKOFF
748
758
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffio
3
- Version: 0.1.49
3
+ Version: 0.1.51
4
4
  Summary: Python SDK for the Diffio API.
5
5
  Author: Diffio
6
6
  License: MIT
@@ -58,6 +58,14 @@ export DIFFIO_API_BASE_URL="https://api.diffio.ai/v1"
58
58
  Use request options to override headers, timeouts, retries, or the API key per request.
59
59
  You can also pass `timeoutInSeconds` as an alias for `timeout`.
60
60
 
61
+ Generation creation is retried only when you supply a non-empty `idempotencyKey`,
62
+ even when `maxRetries` is configured globally or per request. Without a key, a
63
+ timeout or lost response can hide an accepted generation, so the SDK returns the
64
+ error after the first attempt. With a key, retries send the same key and payload.
65
+ Reuse that key when manually retrying the same operation; use a new key for a new
66
+ generation. The audio isolation and restore helpers do not supply a key and do
67
+ not automatically retry their generation-creation step.
68
+
61
69
  ```py
62
70
  from diffio import DiffioClient, RequestOptions
63
71
 
@@ -90,6 +98,7 @@ generation = client.create_generation(
90
98
  model="diffio-3.5",
91
99
  sampling={"steps": 12, "guidance": 1.5},
92
100
  idempotencyKey="restore-job-2026-001",
101
+ requestOptions={"maxRetries": 2},
93
102
  )
94
103
 
95
104
  print(generation.generationId)
@@ -141,6 +150,11 @@ print(info["apiProjectId"], info["generationId"])
141
150
 
142
151
  ## Generation progress
143
152
 
153
+ `wait_for_generation` and `generations.wait_for_complete` wait for the overall
154
+ `status` to become `complete`. Individual stages can reach 100% while video
155
+ restoration or final settlement is still pending; stage progress alone does not
156
+ indicate overall completion.
157
+
144
158
  ```py
145
159
  from diffio import DiffioClient
146
160
 
@@ -7,6 +7,7 @@ import pytest
7
7
 
8
8
  from diffio import DiffioClient, ModelKey
9
9
  from diffio.client import MODEL_ENDPOINTS
10
+ from diffio.errors import DiffioApiError
10
11
 
11
12
 
12
13
  def test_create_project_payload_and_headers(tmp_path: Path):
@@ -997,3 +998,124 @@ def test_webhooks_send_test_event_rejects_invalid_type():
997
998
  eventType="generation.unknown",
998
999
  mode="test",
999
1000
  )
1001
+
1002
+
1003
+ @pytest.mark.parametrize("failure_kind", ["status", "transport"])
1004
+ @pytest.mark.parametrize("model", list(MODEL_ENDPOINTS))
1005
+ @pytest.mark.parametrize("options_scope", ["client", "request"])
1006
+ @pytest.mark.parametrize("idempotency_key", [None, "", " ", 123])
1007
+ def test_create_generation_does_not_retry_without_usable_idempotency_key(
1008
+ failure_kind, model, options_scope, idempotency_key
1009
+ ):
1010
+ calls = []
1011
+
1012
+ def handler(request):
1013
+ calls.append(request)
1014
+ if failure_kind == "transport":
1015
+ raise httpx.ReadTimeout("Response lost after acceptance", request=request)
1016
+ return httpx.Response(503, json={"error": "unavailable"})
1017
+
1018
+ retry_options = {"maxRetries": 2, "retryBackoff": 0}
1019
+ with httpx.Client(base_url="https://api.test", transport=httpx.MockTransport(handler)) as http_client:
1020
+ client = DiffioClient(
1021
+ apiKey="diffio_live_test",
1022
+ baseUrl="https://api.test",
1023
+ httpClient=http_client,
1024
+ requestOptions=retry_options if options_scope == "client" else None,
1025
+ )
1026
+ expected_error = httpx.ReadTimeout if failure_kind == "transport" else DiffioApiError
1027
+ with pytest.raises(expected_error):
1028
+ client.generations.create(
1029
+ apiProjectId="proj",
1030
+ model=model,
1031
+ idempotencyKey=idempotency_key,
1032
+ requestOptions=retry_options if options_scope == "request" else None,
1033
+ )
1034
+
1035
+ assert len(calls) == 1
1036
+
1037
+
1038
+ @pytest.mark.parametrize("failure_kind", ["status", "transport"])
1039
+ @pytest.mark.parametrize("model", list(MODEL_ENDPOINTS))
1040
+ @pytest.mark.parametrize("options_scope", ["client", "request"])
1041
+ def test_create_generation_retries_same_idempotent_request(failure_kind, model, options_scope):
1042
+ calls = []
1043
+
1044
+ def handler(request):
1045
+ calls.append(request.content)
1046
+ if len(calls) == 1:
1047
+ if failure_kind == "transport":
1048
+ raise httpx.ReadTimeout("Response lost after acceptance", request=request)
1049
+ return httpx.Response(503, json={"error": "unavailable"})
1050
+ return httpx.Response(
1051
+ 200,
1052
+ json={
1053
+ "generationId": "gen_original",
1054
+ "apiProjectId": "proj",
1055
+ "modelKey": model,
1056
+ "status": "queued",
1057
+ "idempotentReplay": True,
1058
+ },
1059
+ )
1060
+
1061
+ retry_options = {"maxRetries": 2, "retryBackoff": 0}
1062
+ with httpx.Client(base_url="https://api.test", transport=httpx.MockTransport(handler)) as http_client:
1063
+ client = DiffioClient(
1064
+ apiKey="diffio_live_test",
1065
+ baseUrl="https://api.test",
1066
+ httpClient=http_client,
1067
+ requestOptions=retry_options if options_scope == "client" else None,
1068
+ )
1069
+ generation = client.generations.create(
1070
+ apiProjectId="proj",
1071
+ model=model,
1072
+ idempotencyKey="customer-operation-1",
1073
+ sampling={"steps": 12},
1074
+ params={"seed": 42},
1075
+ requestOptions=retry_options if options_scope == "request" else None,
1076
+ )
1077
+
1078
+ assert len(calls) == 2
1079
+ assert calls[0] == calls[1]
1080
+ assert json.loads(calls[1])["idempotencyKey"] == "customer-operation-1"
1081
+ assert generation.generationId == "gen_original"
1082
+ assert generation.idempotentReplay is True
1083
+
1084
+
1085
+ def test_wait_for_generation_waits_for_overall_completion_after_stage_completion():
1086
+ responses = [
1087
+ ("pending", "pending", 0),
1088
+ ("processing", "running", 20),
1089
+ ("processing", "complete", 100),
1090
+ ("complete", "complete", 100),
1091
+ ]
1092
+ progress_calls = []
1093
+
1094
+ def handler(request):
1095
+ status, video_status, video_progress = responses.pop(0)
1096
+ return httpx.Response(
1097
+ 200,
1098
+ json={
1099
+ "generationId": "gen_video",
1100
+ "apiProjectId": "proj",
1101
+ "status": status,
1102
+ "hasVideo": True,
1103
+ "preProcessing": {"status": "complete", "progress": 100},
1104
+ "inference": {"status": "complete", "progress": 100},
1105
+ "restoredVideo": {"status": video_status, "progress": video_progress},
1106
+ },
1107
+ )
1108
+
1109
+ with httpx.Client(base_url="https://api.test", transport=httpx.MockTransport(handler)) as http_client:
1110
+ client = DiffioClient(apiKey="diffio_live_test", httpClient=http_client)
1111
+ progress = client.generations.wait_for_complete(
1112
+ generationId="gen_video",
1113
+ apiProjectId="proj",
1114
+ pollInterval=0,
1115
+ timeout=5,
1116
+ onProgress=lambda progress: progress_calls.append(progress.status),
1117
+ )
1118
+
1119
+ assert progress.status == "complete"
1120
+ assert progress_calls == ["pending", "processing", "processing", "complete"]
1121
+ assert not responses
File without changes
File without changes
File without changes
File without changes
File without changes