csrd-delegate 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ from ._base_delegate import BaseDelegate
2
+ from ._retry import RetryProfile
3
+
4
+ __all__ = ("BaseDelegate", "RetryProfile")
@@ -0,0 +1,566 @@
1
+ import logging
2
+ from collections.abc import Callable
3
+ from contextlib import asynccontextmanager
4
+ from copy import deepcopy
5
+ from inspect import signature
6
+ from types import TracebackType
7
+ from typing import Any, Self
8
+
9
+ import httpx
10
+ from fastapi import HTTPException
11
+ from httpx import AsyncClient, ConnectError, Response, TimeoutException
12
+ from httpx._client import USE_CLIENT_DEFAULT, UseClientDefault
13
+ from httpx._types import (
14
+ AuthTypes,
15
+ CookieTypes,
16
+ HeaderTypes,
17
+ QueryParamTypes,
18
+ RequestContent,
19
+ RequestData,
20
+ RequestExtensions,
21
+ RequestFiles,
22
+ TimeoutTypes,
23
+ URLTypes,
24
+ )
25
+ from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_attempt, wait_exponential
26
+ from yarl import URL
27
+
28
+ from csrd.context import get_headers
29
+ from csrd.delegate._response_types import ModelHandler, ResponseHandlerMap
30
+ from csrd.delegate._retry import RETRY_PROFILES, RetryProfiles
31
+ from csrd.models.model_parser import ModelParserMixin, ParsedResponse, ResponseModelType
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class BaseDelegate(ModelParserMixin):
37
+ """
38
+ A base HTTP delegate class for managing outbound HTTP requests with retry logic,
39
+ dynamic response parsing, and FastAPI integration.
40
+ """
41
+
42
+ _filter_headers_list: list[str]
43
+ _client: AsyncClient
44
+ _base_url: URL
45
+ _ignore_incoming_headers: bool
46
+
47
+ _retry_enabled: bool
48
+ _retry_profile: RetryProfiles | None
49
+ _retry_attempts: int
50
+ _retry_backoff: float
51
+
52
+ _owns_client: bool
53
+
54
+ def __init__(
55
+ self,
56
+ service_host: str | URL,
57
+ *,
58
+ client: AsyncClient | None = None,
59
+ header_filter_list: list[str] | None = None,
60
+ ignore_incoming_headers=False,
61
+ retry_enabled: bool = False,
62
+ retry_profile: RetryProfiles | None = None,
63
+ retry_attempts: int = 3,
64
+ retry_backoff: float = 0.2,
65
+ ):
66
+ super().__init__()
67
+ self._base_url = URL(service_host)
68
+ self._owns_client = client is None
69
+ self._client = client or AsyncClient(base_url=str(self._base_url))
70
+
71
+ self._filter_headers_list = header_filter_list or ["host"]
72
+ self._ignore_incoming_headers = ignore_incoming_headers
73
+
74
+ self._retry_enabled = retry_enabled or (retry_profile is not None)
75
+ self._retry_profile = retry_profile
76
+ self._retry_attempts = retry_attempts
77
+ self._retry_backoff = retry_backoff
78
+
79
+ # ── Lifecycle ────────────────────────────────────────────────────────
80
+
81
+ async def close(self) -> None:
82
+ """Close the underlying HTTP client if this delegate owns it.
83
+
84
+ Safe to call multiple times. If the caller injected an external
85
+ ``AsyncClient``, this is a no-op (the caller owns that client).
86
+ """
87
+ if self._owns_client and not self._client.is_closed:
88
+ await self._client.aclose()
89
+
90
+ async def __aenter__(self) -> Self:
91
+ return self
92
+
93
+ async def __aexit__(
94
+ self,
95
+ exc_type: type[BaseException] | None,
96
+ exc_val: BaseException | None,
97
+ exc_tb: TracebackType | None,
98
+ ) -> None:
99
+ await self.close()
100
+
101
+ @asynccontextmanager
102
+ async def _request_context(self, method: Callable, **kwargs):
103
+ headers = self._headers or {}
104
+ headers = self._normalize_headers(headers)
105
+
106
+ if kwargs.get("headers"):
107
+ user_headers = self._normalize_headers(kwargs["headers"])
108
+ headers.update(user_headers)
109
+
110
+ kwargs["headers"] = self._filter_headers(headers)
111
+ retry_config = self._parse_retry_profile(**kwargs)
112
+
113
+ kwargs = self._filter_method_kwargs(method, **kwargs)
114
+ try:
115
+ response = await self._call_with_optional_retry(method, **kwargs, **retry_config)
116
+ yield response
117
+ finally:
118
+ pass
119
+
120
+ async def _call_with_optional_retry(
121
+ self,
122
+ method: Callable,
123
+ *,
124
+ retry_enabled: bool | None = None,
125
+ retry_attempts: int | None = None,
126
+ retry_backoff_base: float | None = None,
127
+ **kwargs,
128
+ ) -> Response:
129
+ use_retry = self._retry_enabled if retry_enabled is None else retry_enabled
130
+ attempts = retry_attempts or self._retry_attempts
131
+ backoff = retry_backoff_base or self._retry_backoff
132
+ kwargs = self._filter_method_kwargs(method, **kwargs)
133
+
134
+ async def _do_req(**kwargs_) -> Response:
135
+ try:
136
+ response_ = await method(**kwargs_)
137
+ return self._parse_status_code(
138
+ response_, response_handlers=kwargs_.get("response_handlers")
139
+ )
140
+ except Exception as e_:
141
+ ex = HTTPException(
142
+ status_code=getattr(e_, "status_code", 500), detail=getattr(e_, "detail", None)
143
+ )
144
+ logger.warning(ex)
145
+ raise ex from e_
146
+
147
+ if not use_retry:
148
+ return await _do_req(**kwargs)
149
+
150
+ async for attempt in AsyncRetrying(
151
+ stop=stop_after_attempt(attempts),
152
+ wait=wait_exponential(multiplier=backoff),
153
+ retry=retry_if_exception_type(
154
+ (HTTPException, TimeoutException, ConnectError, httpx.HTTPError)
155
+ ),
156
+ reraise=True,
157
+ ):
158
+ with attempt:
159
+ logger.info(f"[attempt {attempt.retry_state.attempt_number}]")
160
+ return await _do_req(**kwargs)
161
+ raise RuntimeError("Unreachable: retry loop exhausted without reraise")
162
+
163
+ async def _request(
164
+ self,
165
+ method: Callable,
166
+ *,
167
+ url: URLTypes,
168
+ params: QueryParamTypes | None = None,
169
+ headers: HeaderTypes | None = None,
170
+ cookies: CookieTypes | None = None,
171
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
172
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
173
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
174
+ extensions: RequestExtensions | None = None,
175
+ content: RequestContent | None = None,
176
+ data: RequestData | None = None,
177
+ files: RequestFiles | None = None,
178
+ json: Any | None = None,
179
+ model_handler: ModelHandler | None = None,
180
+ response_model: ResponseModelType | None = None,
181
+ response_handlers: ResponseHandlerMap | None = None,
182
+ retry_enabled: bool | None = None,
183
+ retry_profile: RetryProfiles | None = None,
184
+ retry_attempts: int | None = None,
185
+ retry_backoff: float | None = None,
186
+ ) -> ParsedResponse:
187
+ async with self._request_context(
188
+ method,
189
+ url=url,
190
+ params=params,
191
+ headers=headers,
192
+ cookies=cookies,
193
+ auth=auth,
194
+ follow_redirects=follow_redirects,
195
+ timeout=timeout,
196
+ extensions=extensions,
197
+ content=content,
198
+ data=data,
199
+ files=files,
200
+ json=json,
201
+ retry_enabled=retry_enabled,
202
+ retry_profile=retry_profile,
203
+ retry_attempts=retry_attempts,
204
+ retry_backoff=retry_backoff,
205
+ ) as response:
206
+ response = self._parse_status_code(response, response_handlers=response_handlers)
207
+ return self.apply_model(response, model=response_model, model_handler=model_handler)
208
+
209
+ # Public HTTP methods
210
+ async def get(
211
+ self,
212
+ url: URLTypes,
213
+ *,
214
+ params: QueryParamTypes | None = None,
215
+ headers: HeaderTypes | None = None,
216
+ cookies: CookieTypes | None = None,
217
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
218
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
219
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
220
+ extensions: RequestExtensions | None = None,
221
+ model_handler: ModelHandler | None = None,
222
+ response_model: ResponseModelType | None = None,
223
+ response_handlers: ResponseHandlerMap | None = None,
224
+ retry_enabled: bool | None = None,
225
+ retry_profile: RetryProfiles | None = None,
226
+ retry_attempts: int | None = None,
227
+ retry_backoff: float | None = None,
228
+ ) -> ParsedResponse:
229
+ return await self._request(
230
+ self._client.get,
231
+ url=url,
232
+ params=params,
233
+ headers=headers,
234
+ cookies=cookies,
235
+ auth=auth,
236
+ follow_redirects=follow_redirects,
237
+ timeout=timeout,
238
+ extensions=extensions,
239
+ model_handler=model_handler,
240
+ response_model=response_model,
241
+ response_handlers=response_handlers,
242
+ retry_enabled=retry_enabled,
243
+ retry_profile=retry_profile,
244
+ retry_attempts=retry_attempts,
245
+ retry_backoff=retry_backoff,
246
+ )
247
+
248
+ async def delete(
249
+ self,
250
+ url: URLTypes,
251
+ *,
252
+ params: QueryParamTypes | None = None,
253
+ headers: HeaderTypes | None = None,
254
+ cookies: CookieTypes | None = None,
255
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
256
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
257
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
258
+ extensions: RequestExtensions | None = None,
259
+ model_handler: ModelHandler | None = None,
260
+ response_model: ResponseModelType | None = None,
261
+ response_handlers: ResponseHandlerMap | None = None,
262
+ retry_enabled: bool | None = None,
263
+ retry_profile: RetryProfiles | None = None,
264
+ retry_attempts: int | None = None,
265
+ retry_backoff: float | None = None,
266
+ ) -> Any | bytes | dict | None:
267
+ return await self._request(
268
+ self._client.delete,
269
+ url=url,
270
+ params=params,
271
+ headers=headers,
272
+ cookies=cookies,
273
+ auth=auth,
274
+ follow_redirects=follow_redirects,
275
+ timeout=timeout,
276
+ extensions=extensions,
277
+ model_handler=model_handler,
278
+ response_model=response_model,
279
+ response_handlers=response_handlers,
280
+ retry_enabled=retry_enabled,
281
+ retry_profile=retry_profile,
282
+ retry_attempts=retry_attempts,
283
+ retry_backoff=retry_backoff,
284
+ )
285
+
286
+ async def head(
287
+ self,
288
+ url: URLTypes,
289
+ *,
290
+ params: QueryParamTypes | None = None,
291
+ headers: HeaderTypes | None = None,
292
+ cookies: CookieTypes | None = None,
293
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
294
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
295
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
296
+ extensions: RequestExtensions | None = None,
297
+ model_handler: ModelHandler | None = None,
298
+ response_model: ResponseModelType | None = None,
299
+ response_handlers: ResponseHandlerMap | None = None,
300
+ retry_enabled: bool | None = None,
301
+ retry_profile: RetryProfiles | None = None,
302
+ retry_attempts: int | None = None,
303
+ retry_backoff: float | None = None,
304
+ ) -> ParsedResponse:
305
+ return await self._request(
306
+ self._client.head,
307
+ url=url,
308
+ params=params,
309
+ headers=headers,
310
+ cookies=cookies,
311
+ auth=auth,
312
+ follow_redirects=follow_redirects,
313
+ timeout=timeout,
314
+ extensions=extensions,
315
+ model_handler=model_handler,
316
+ response_model=response_model,
317
+ response_handlers=response_handlers,
318
+ retry_enabled=retry_enabled,
319
+ retry_profile=retry_profile,
320
+ retry_attempts=retry_attempts,
321
+ retry_backoff=retry_backoff,
322
+ )
323
+
324
+ async def post(
325
+ self,
326
+ url: URLTypes,
327
+ *,
328
+ content: RequestContent | None = None,
329
+ data: RequestData | None = None,
330
+ files: RequestFiles | None = None,
331
+ json: Any | None = None,
332
+ params: QueryParamTypes | None = None,
333
+ headers: HeaderTypes | None = None,
334
+ cookies: CookieTypes | None = None,
335
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
336
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
337
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
338
+ extensions: RequestExtensions | None = None,
339
+ model_handler: ModelHandler | None = None,
340
+ response_model: ResponseModelType | None = None,
341
+ response_handlers: ResponseHandlerMap | None = None,
342
+ retry_enabled: bool | None = None,
343
+ retry_profile: RetryProfiles | None = None,
344
+ retry_attempts: int | None = None,
345
+ retry_backoff: float | None = None,
346
+ ) -> ParsedResponse:
347
+ return await self._request(
348
+ self._client.post,
349
+ url=url,
350
+ params=params,
351
+ headers=headers,
352
+ cookies=cookies,
353
+ auth=auth,
354
+ follow_redirects=follow_redirects,
355
+ timeout=timeout,
356
+ extensions=extensions,
357
+ content=content,
358
+ data=data,
359
+ files=files,
360
+ json=json,
361
+ model_handler=model_handler,
362
+ response_model=response_model,
363
+ response_handlers=response_handlers,
364
+ retry_enabled=retry_enabled,
365
+ retry_profile=retry_profile,
366
+ retry_attempts=retry_attempts,
367
+ retry_backoff=retry_backoff,
368
+ )
369
+
370
+ async def put(
371
+ self,
372
+ url: URLTypes,
373
+ *,
374
+ content: RequestContent | None = None,
375
+ data: RequestData | None = None,
376
+ files: RequestFiles | None = None,
377
+ json: Any | None = None,
378
+ params: QueryParamTypes | None = None,
379
+ headers: HeaderTypes | None = None,
380
+ cookies: CookieTypes | None = None,
381
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
382
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
383
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
384
+ extensions: RequestExtensions | None = None,
385
+ model_handler: ModelHandler | None = None,
386
+ response_model: ResponseModelType | None = None,
387
+ response_handlers: ResponseHandlerMap | None = None,
388
+ retry_enabled: bool | None = None,
389
+ retry_profile: RetryProfiles | None = None,
390
+ retry_attempts: int | None = None,
391
+ retry_backoff: float | None = None,
392
+ ) -> ParsedResponse:
393
+ return await self._request(
394
+ self._client.put,
395
+ url=url,
396
+ content=content,
397
+ data=data,
398
+ files=files,
399
+ json=json,
400
+ params=params,
401
+ headers=headers,
402
+ cookies=cookies,
403
+ auth=auth,
404
+ follow_redirects=follow_redirects,
405
+ timeout=timeout,
406
+ extensions=extensions,
407
+ model_handler=model_handler,
408
+ response_model=response_model,
409
+ response_handlers=response_handlers,
410
+ retry_enabled=retry_enabled,
411
+ retry_profile=retry_profile,
412
+ retry_attempts=retry_attempts,
413
+ retry_backoff=retry_backoff,
414
+ )
415
+
416
+ async def patch(
417
+ self,
418
+ url: URLTypes,
419
+ *,
420
+ content: RequestContent | None = None,
421
+ data: RequestData | None = None,
422
+ files: RequestFiles | None = None,
423
+ json: Any | None = None,
424
+ params: QueryParamTypes | None = None,
425
+ headers: HeaderTypes | None = None,
426
+ cookies: CookieTypes | None = None,
427
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
428
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
429
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
430
+ extensions: RequestExtensions | None = None,
431
+ model_handler: ModelHandler | None = None,
432
+ response_model: ResponseModelType | None = None,
433
+ response_handlers: ResponseHandlerMap | None = None,
434
+ retry_enabled: bool | None = None,
435
+ retry_profile: RetryProfiles | None = None,
436
+ retry_attempts: int | None = None,
437
+ retry_backoff: float | None = None,
438
+ ) -> ParsedResponse:
439
+ return await self._request(
440
+ self._client.patch,
441
+ url=url,
442
+ params=params,
443
+ headers=headers,
444
+ cookies=cookies,
445
+ auth=auth,
446
+ follow_redirects=follow_redirects,
447
+ timeout=timeout,
448
+ extensions=extensions,
449
+ content=content,
450
+ data=data,
451
+ files=files,
452
+ json=json,
453
+ model_handler=model_handler,
454
+ response_model=response_model,
455
+ response_handlers=response_handlers,
456
+ retry_enabled=retry_enabled,
457
+ retry_profile=retry_profile,
458
+ retry_attempts=retry_attempts,
459
+ retry_backoff=retry_backoff,
460
+ )
461
+
462
+ async def options(
463
+ self,
464
+ url: URLTypes,
465
+ *,
466
+ params: QueryParamTypes | None = None,
467
+ headers: HeaderTypes | None = None,
468
+ cookies: CookieTypes | None = None,
469
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
470
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
471
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
472
+ extensions: RequestExtensions | None = None,
473
+ model_handler: ModelHandler | None = None,
474
+ response_model: ResponseModelType | None = None,
475
+ response_handlers: ResponseHandlerMap | None = None,
476
+ retry_enabled: bool | None = None,
477
+ retry_profile: RetryProfiles | None = None,
478
+ retry_attempts: int | None = None,
479
+ retry_backoff: float | None = None,
480
+ ) -> ParsedResponse:
481
+ return await self._request(
482
+ self._client.options,
483
+ url=url,
484
+ params=params,
485
+ headers=headers,
486
+ cookies=cookies,
487
+ auth=auth,
488
+ follow_redirects=follow_redirects,
489
+ timeout=timeout,
490
+ extensions=extensions,
491
+ model_handler=model_handler,
492
+ response_model=response_model,
493
+ response_handlers=response_handlers,
494
+ retry_enabled=retry_enabled,
495
+ retry_profile=retry_profile,
496
+ retry_attempts=retry_attempts,
497
+ retry_backoff=retry_backoff,
498
+ )
499
+
500
+ # Helpers
501
+ @property
502
+ def _headers(self) -> dict:
503
+ if self._ignore_incoming_headers:
504
+ return {}
505
+ headers = get_headers()
506
+ return self._filter_headers(dict(headers))
507
+
508
+ def _filter_headers(self, headers: dict) -> dict:
509
+ return {k: v for k, v in headers.items() if k.lower() not in self._filter_headers_list}
510
+
511
+ def _parse_retry_profile(self, **kwargs) -> dict:
512
+ retry_profile = kwargs.pop("retry_profile", self._retry_profile)
513
+ profile_config = deepcopy(RETRY_PROFILES.get(retry_profile, {})) if retry_profile else {}
514
+
515
+ return {
516
+ "retry_enabled": kwargs.pop("retry_enabled", None)
517
+ or profile_config.get("retry_enabled")
518
+ or self._retry_enabled,
519
+ "retry_attempts": kwargs.pop("retry_attempts", None)
520
+ or profile_config.get("retry_attempts")
521
+ or self._retry_attempts,
522
+ "retry_backoff": kwargs.pop("retry_backoff", None)
523
+ or profile_config.get("retry_backoff")
524
+ or self._retry_backoff,
525
+ }
526
+
527
+ @staticmethod
528
+ def _filter_method_kwargs(method: Callable, **kwargs) -> dict:
529
+ valid_params = set(signature(method).parameters)
530
+ return {k: v for k, v in kwargs.items() if v is not None and k in valid_params}
531
+
532
+ @staticmethod
533
+ def _normalize_headers(headers: dict) -> dict:
534
+ result = {}
535
+ for k, v in headers.items():
536
+ lower = k.lower()
537
+ if lower not in result:
538
+ result[lower] = v
539
+ return result
540
+
541
+ @staticmethod
542
+ def _parse_status_code(
543
+ response: Response,
544
+ *,
545
+ response_handlers: dict[int, Callable[[Response], Response | None]] | None = None,
546
+ ) -> Response:
547
+ # Prefer handler override if explicitly provided
548
+ if response_handlers and response.status_code in response_handlers:
549
+ result = response_handlers[response.status_code](response)
550
+ return result if result is not None else response
551
+
552
+ # Fast-path success
553
+ if 200 <= response.status_code <= 299:
554
+ return response
555
+
556
+ try:
557
+ detail = response.json()
558
+ except Exception:
559
+ detail = None
560
+
561
+ if isinstance(detail, dict):
562
+ message = detail.get("detail", f"Unexpected status code: {response.status_code}")
563
+ else:
564
+ message = str(detail) if detail else f"Unexpected status code: {response.status_code}"
565
+
566
+ raise HTTPException(status_code=response.status_code, detail=message)
@@ -0,0 +1,14 @@
1
+ """HTTP response-specific types that depend on httpx."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from httpx import Response
7
+
8
+ from csrd.models.model_parser._types import ResponseModelType
9
+
10
+ ResponseHandler = Callable[[Response], Response | None]
11
+ ResponseHandlerMap = dict[int, ResponseHandler]
12
+ ModelHandler = Callable[[Response, ResponseModelType | None], Any]
13
+
14
+ __all__ = ("ModelHandler", "ResponseHandler", "ResponseHandlerMap")
@@ -0,0 +1,17 @@
1
+ from typing import Literal, TypedDict
2
+
3
+ RetryProfiles = Literal["no_retry", "conservative", "aggressive", "resilient"] | None
4
+
5
+
6
+ class RetryProfile(TypedDict, total=False):
7
+ retry_enabled: bool
8
+ retry_attempts: int | None
9
+ retry_backoff: float | None
10
+
11
+
12
+ RETRY_PROFILES: dict[RetryProfiles, RetryProfile] = {
13
+ "no_retry": {"retry_enabled": False},
14
+ "conservative": {"retry_enabled": True, "retry_attempts": 2, "retry_backoff": 0.5},
15
+ "aggressive": {"retry_enabled": True, "retry_attempts": 5, "retry_backoff": 0.3},
16
+ "resilient": {"retry_enabled": True, "retry_attempts": 7, "retry_backoff": 0.6},
17
+ }
csrd/delegate/py.typed ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: csrd-delegate
3
+ Version: 0.1.0
4
+ Summary: HTTP delegate base class with retry support
5
+ Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
+ Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/delegate
7
+ Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: csrd-context
11
+ Requires-Dist: csrd-models
12
+ Requires-Dist: fastapi<1,>=0.115
13
+ Requires-Dist: httpx<1,>=0.28
14
+ Requires-Dist: tenacity<10,>=9.1
15
+ Requires-Dist: yarl<2,>=1.20
@@ -0,0 +1,8 @@
1
+ csrd/delegate/__init__.py,sha256=kt_Tyeqi3z7lMJlsMEjKegO1cO0A6lL0QVJdvYumeS8,118
2
+ csrd/delegate/_base_delegate.py,sha256=3T2zXywo6kNm7RbhGq8Viis9X0a-fAc2we3eAT4o1t4,20974
3
+ csrd/delegate/_response_types.py,sha256=z5ECH1tDnBGSvMUPWtS_6nwlX7oG1GmxeI82yDGejSc,450
4
+ csrd/delegate/_retry.py,sha256=DjGAuXfgMD1o_t5nIp0e4xe9T__eYs9EmhnbSmBdzIg,617
5
+ csrd/delegate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ csrd_delegate-0.1.0.dist-info/METADATA,sha256=sJOu9Je_EpRwq5TTmuY2zsNyu9EA5pYT2avXN9LYNJ8,584
7
+ csrd_delegate-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ csrd_delegate-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any