66from nisystemlink .clients .core import ApiException
77from nisystemlink .clients .core ._uplink ._base_client import _handle_http_status
88from nisystemlink .clients .core ._uplink ._multipart_retry import (
9+ _RetryableMultipartCleanupTemplate ,
910 _RetryableMultipartRequestTemplate ,
1011 retryable_multipart_request ,
1112)
1213from requests import Response
1314from uplink import Consumer , Part , post , retry
15+ from uplink .clients .io import CompositeRequestTemplate
1416from uplink .clients .io import state as uplink_state
17+ from uplink .clients .io .interfaces import RequestTemplate
1518
1619
1720class _NonSeekableStream :
@@ -33,6 +36,18 @@ def seek(self, offset: int) -> None:
3336 raise OSError ("cannot rewind" )
3437
3538
39+ class _StaticTransitionTemplate (RequestTemplate ):
40+ def __init__ (self , response_transition = None , exception_transition = None ) -> None :
41+ self ._response_transition = response_transition
42+ self ._exception_transition = exception_transition
43+
44+ def after_response (self , request , response ):
45+ return self ._response_transition
46+
47+ def after_exception (self , request , exc_type , exc_val , exc_tb ):
48+ return self ._exception_transition
49+
50+
3651@retry (
3752 when = retry .when .status (429 ),
3853 stop = retry .stop .after_attempt (2 ),
@@ -139,6 +154,92 @@ def test__before_request_when_rewind_fails_after_response__finishes_with_saved_r
139154 next_state = action (uplink_state .BeforeRequest (request ))
140155 assert isinstance (next_state , uplink_state .Finish )
141156 assert next_state .response is response
157+ request_id = id (request )
158+ assert request_id not in template ._attempted_request_ids
159+ assert request_id not in template ._responses_by_request_id
160+ assert request_id not in template ._exceptions_by_request_id
161+
162+ def test__terminal_response_after_retry_pipeline__clears_saved_retry_state (self ):
163+ request = (
164+ "POST" ,
165+ "https://example.com/upload" ,
166+ {
167+ "files" : {
168+ "artifact" : ("artifact.bin" , io .BytesIO (b"artifact" )),
169+ }
170+ },
171+ )
172+ response = Response ()
173+ response .status_code = 200
174+ response .url = "https://example.com/upload"
175+ template = _RetryableMultipartRequestTemplate ()
176+ composite = CompositeRequestTemplate (
177+ [template , _StaticTransitionTemplate (), _RetryableMultipartCleanupTemplate (template )]
178+ )
179+
180+ template .before_request (request )
181+ composite .after_response (request , response )
182+
183+ request_id = id (request )
184+ assert request_id not in template ._attempted_request_ids
185+ assert request_id not in template ._responses_by_request_id
186+ assert request_id not in template ._exceptions_by_request_id
187+
188+ def test__retry_transition_after_response__preserves_saved_retry_state (self ):
189+ request = (
190+ "POST" ,
191+ "https://example.com/upload" ,
192+ {
193+ "files" : {
194+ "artifact" : ("artifact.bin" , io .BytesIO (b"artifact" )),
195+ }
196+ },
197+ )
198+ response = Response ()
199+ response .status_code = 503
200+ response .url = "https://example.com/upload"
201+ retry_transition = object ()
202+ template = _RetryableMultipartRequestTemplate ()
203+ composite = CompositeRequestTemplate (
204+ [
205+ template ,
206+ _StaticTransitionTemplate (response_transition = retry_transition ),
207+ _RetryableMultipartCleanupTemplate (template ),
208+ ]
209+ )
210+
211+ template .before_request (request )
212+
213+ assert composite .after_response (request , response ) is retry_transition
214+
215+ request_id = id (request )
216+ assert request_id in template ._attempted_request_ids
217+ assert template ._responses_by_request_id [request_id ] is response
218+ assert request_id not in template ._exceptions_by_request_id
219+
220+ def test__terminal_exception_after_retry_pipeline__clears_saved_retry_state (self ):
221+ request = (
222+ "POST" ,
223+ "https://example.com/upload" ,
224+ {
225+ "files" : {
226+ "artifact" : ("artifact.bin" , io .BytesIO (b"artifact" )),
227+ }
228+ },
229+ )
230+ exception = RuntimeError ("boom" )
231+ template = _RetryableMultipartRequestTemplate ()
232+ composite = CompositeRequestTemplate (
233+ [template , _StaticTransitionTemplate (), _RetryableMultipartCleanupTemplate (template )]
234+ )
235+
236+ template .before_request (request )
237+ composite .after_exception (request , RuntimeError , exception , None )
238+
239+ request_id = id (request )
240+ assert request_id not in template ._attempted_request_ids
241+ assert request_id not in template ._responses_by_request_id
242+ assert request_id not in template ._exceptions_by_request_id
142243
143244 def test__before_request_on_retry_with_string_only_parts__allows_retry (self ):
144245 request = (
0 commit comments