kard-financial-sdk 0.0.82__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.
Files changed (302) hide show
  1. kard/__init__.py +579 -0
  2. kard/auth/__init__.py +34 -0
  3. kard/auth/client.py +121 -0
  4. kard/auth/raw_client.py +108 -0
  5. kard/auth/types/__init__.py +34 -0
  6. kard/auth/types/token_response.py +25 -0
  7. kard/client.py +416 -0
  8. kard/commons/__init__.py +120 -0
  9. kard/commons/errors/__init__.py +44 -0
  10. kard/commons/errors/conflict_error.py +11 -0
  11. kard/commons/errors/does_not_exist_error.py +11 -0
  12. kard/commons/errors/internal_server_error.py +11 -0
  13. kard/commons/errors/invalid_request.py +11 -0
  14. kard/commons/errors/unauthorized_error.py +11 -0
  15. kard/commons/types/__init__.py +107 -0
  16. kard/commons/types/category_option.py +26 -0
  17. kard/commons/types/commission_type.py +5 -0
  18. kard/commons/types/commission_value.py +28 -0
  19. kard/commons/types/commission_value_type.py +5 -0
  20. kard/commons/types/empty_object.py +17 -0
  21. kard/commons/types/enrolled_rewards_type.py +5 -0
  22. kard/commons/types/error_object.py +43 -0
  23. kard/commons/types/error_response.py +20 -0
  24. kard/commons/types/error_source.py +32 -0
  25. kard/commons/types/job.py +24 -0
  26. kard/commons/types/job_response.py +27 -0
  27. kard/commons/types/job_status.py +5 -0
  28. kard/commons/types/links.py +27 -0
  29. kard/commons/types/mongo_id.py +3 -0
  30. kard/commons/types/notification_type.py +19 -0
  31. kard/commons/types/organization_id.py +3 -0
  32. kard/commons/types/purchase_channel.py +5 -0
  33. kard/commons/types/relationship_data.py +24 -0
  34. kard/commons/types/relationship_multiple.py +20 -0
  35. kard/commons/types/relationship_single.py +20 -0
  36. kard/commons/types/resource_type.py +3 -0
  37. kard/commons/types/state.py +68 -0
  38. kard/commons/types/subscription_id.py +3 -0
  39. kard/commons/types/user_id.py +3 -0
  40. kard/core/__init__.py +105 -0
  41. kard/core/api_error.py +23 -0
  42. kard/core/client_wrapper.py +97 -0
  43. kard/core/datetime_utils.py +28 -0
  44. kard/core/file.py +67 -0
  45. kard/core/force_multipart.py +18 -0
  46. kard/core/http_client.py +613 -0
  47. kard/core/http_response.py +55 -0
  48. kard/core/http_sse/__init__.py +42 -0
  49. kard/core/http_sse/_api.py +112 -0
  50. kard/core/http_sse/_decoders.py +61 -0
  51. kard/core/http_sse/_exceptions.py +7 -0
  52. kard/core/http_sse/_models.py +17 -0
  53. kard/core/jsonable_encoder.py +100 -0
  54. kard/core/oauth_token_provider.py +73 -0
  55. kard/core/pydantic_utilities.py +260 -0
  56. kard/core/query_encoder.py +58 -0
  57. kard/core/remove_none_from_dict.py +11 -0
  58. kard/core/request_options.py +35 -0
  59. kard/core/serialization.py +276 -0
  60. kard/environment.py +8 -0
  61. kard/files/__init__.py +58 -0
  62. kard/files/client.py +213 -0
  63. kard/files/errors/__init__.py +34 -0
  64. kard/files/errors/forbidden_error.py +11 -0
  65. kard/files/raw_client.py +278 -0
  66. kard/files/types/__init__.py +53 -0
  67. kard/files/types/file_metadata_attribute.py +39 -0
  68. kard/files/types/file_metadata_with_url.py +34 -0
  69. kard/files/types/file_type.py +13 -0
  70. kard/files/types/files_metadata_sort_options.py +5 -0
  71. kard/files/types/get_files_metadata_response.py +71 -0
  72. kard/files/types/pagination_meta.py +29 -0
  73. kard/notifications/__init__.py +279 -0
  74. kard/notifications/client.py +63 -0
  75. kard/notifications/raw_client.py +13 -0
  76. kard/notifications/subscriptions/__init__.py +97 -0
  77. kard/notifications/subscriptions/client.py +372 -0
  78. kard/notifications/subscriptions/raw_client.py +581 -0
  79. kard/notifications/subscriptions/types/__init__.py +94 -0
  80. kard/notifications/subscriptions/types/create_subscription_union.py +27 -0
  81. kard/notifications/subscriptions/types/create_subscriptions_response_object.py +43 -0
  82. kard/notifications/subscriptions/types/created_subscription.py +23 -0
  83. kard/notifications/subscriptions/types/subscription.py +25 -0
  84. kard/notifications/subscriptions/types/subscription_attributes.py +35 -0
  85. kard/notifications/subscriptions/types/subscription_request.py +20 -0
  86. kard/notifications/subscriptions/types/subscription_request_attributes.py +35 -0
  87. kard/notifications/subscriptions/types/subscription_request_body.py +42 -0
  88. kard/notifications/subscriptions/types/subscription_request_union.py +26 -0
  89. kard/notifications/subscriptions/types/subscription_union.py +27 -0
  90. kard/notifications/subscriptions/types/subscriptions_response_object.py +43 -0
  91. kard/notifications/subscriptions/types/update_subscription_request.py +20 -0
  92. kard/notifications/subscriptions/types/update_subscription_request_attributes.py +39 -0
  93. kard/notifications/subscriptions/types/update_subscription_request_body.py +40 -0
  94. kard/notifications/subscriptions/types/update_subscription_request_union.py +26 -0
  95. kard/notifications/subscriptions/types/update_subscriptions_response_object.py +41 -0
  96. kard/notifications/types/__init__.py +214 -0
  97. kard/notifications/types/audit_update_attributes.py +88 -0
  98. kard/notifications/types/audit_update_data.py +27 -0
  99. kard/notifications/types/audit_update_relationships.py +21 -0
  100. kard/notifications/types/broker_amount.py +28 -0
  101. kard/notifications/types/broker_amount_type.py +5 -0
  102. kard/notifications/types/broker_asset.py +33 -0
  103. kard/notifications/types/broker_asset_type.py +5 -0
  104. kard/notifications/types/broker_operation_hours.py +30 -0
  105. kard/notifications/types/broker_operation_period.py +28 -0
  106. kard/notifications/types/broker_purchase_channel.py +5 -0
  107. kard/notifications/types/broker_reward.py +28 -0
  108. kard/notifications/types/broker_reward_type.py +5 -0
  109. kard/notifications/types/clawback_data.py +27 -0
  110. kard/notifications/types/earned_reward_approved_data.py +27 -0
  111. kard/notifications/types/earned_reward_attributes.py +18 -0
  112. kard/notifications/types/earned_reward_relationships.py +21 -0
  113. kard/notifications/types/earned_reward_settled_attributes.py +23 -0
  114. kard/notifications/types/earned_reward_settled_data.py +27 -0
  115. kard/notifications/types/failed_transaction_attributes.py +41 -0
  116. kard/notifications/types/failed_transaction_data.py +27 -0
  117. kard/notifications/types/failed_transaction_relationships.py +22 -0
  118. kard/notifications/types/location_address.py +39 -0
  119. kard/notifications/types/location_coordinates.py +27 -0
  120. kard/notifications/types/location_status.py +5 -0
  121. kard/notifications/types/merchant_source.py +5 -0
  122. kard/notifications/types/notification_data_union.py +203 -0
  123. kard/notifications/types/notification_metadata.py +22 -0
  124. kard/notifications/types/notification_payload.py +65 -0
  125. kard/notifications/types/offer_status.py +5 -0
  126. kard/notifications/types/offer_type.py +5 -0
  127. kard/notifications/types/reward_notification_attributes.py +48 -0
  128. kard/notifications/types/time_period.py +27 -0
  129. kard/notifications/types/transaction_relationships.py +22 -0
  130. kard/notifications/types/user_offer_status.py +5 -0
  131. kard/notifications/types/valid_transaction_attributes.py +25 -0
  132. kard/notifications/types/valid_transaction_commission_earned.py +21 -0
  133. kard/notifications/types/valid_transaction_data.py +27 -0
  134. kard/notifications/types/webhook_locations_attributes.py +71 -0
  135. kard/notifications/types/webhook_locations_data.py +27 -0
  136. kard/notifications/types/webhook_locations_relationships.py +20 -0
  137. kard/notifications/types/webhook_merchant_attributes.py +67 -0
  138. kard/notifications/types/webhook_merchant_data.py +27 -0
  139. kard/notifications/types/webhook_merchant_relationships.py +20 -0
  140. kard/notifications/types/webhook_offer_attributes.py +143 -0
  141. kard/notifications/types/webhook_offer_data.py +27 -0
  142. kard/notifications/types/webhook_offer_relationships.py +20 -0
  143. kard/notifications/types/webhook_user_offer_attributes.py +41 -0
  144. kard/notifications/types/webhook_user_offer_data.py +27 -0
  145. kard/notifications/types/webhook_user_offer_relationships.py +21 -0
  146. kard/ping/__init__.py +39 -0
  147. kard/ping/client.py +100 -0
  148. kard/ping/errors/__init__.py +34 -0
  149. kard/ping/errors/network_blocked_error.py +11 -0
  150. kard/ping/raw_client.py +113 -0
  151. kard/ping/types/__init__.py +38 -0
  152. kard/ping/types/network_blocked_error_body.py +34 -0
  153. kard/ping/types/ping_response_object.py +46 -0
  154. kard/py.typed +0 -0
  155. kard/transactions/__init__.py +212 -0
  156. kard/transactions/client.py +639 -0
  157. kard/transactions/errors/__init__.py +40 -0
  158. kard/transactions/errors/create_audit_multi_status.py +11 -0
  159. kard/transactions/errors/create_incoming_transactions_multi_status.py +11 -0
  160. kard/transactions/errors/fraud_multi_status.py +11 -0
  161. kard/transactions/raw_client.py +925 -0
  162. kard/transactions/types/__init__.py +199 -0
  163. kard/transactions/types/audit_attributes.py +44 -0
  164. kard/transactions/types/audit_request_data.py +20 -0
  165. kard/transactions/types/audit_response_attributes.py +24 -0
  166. kard/transactions/types/audit_response_data.py +25 -0
  167. kard/transactions/types/audit_status.py +5 -0
  168. kard/transactions/types/card_network.py +5 -0
  169. kard/transactions/types/commission_earned_details.py +21 -0
  170. kard/transactions/types/create_audit_multi_status_response.py +21 -0
  171. kard/transactions/types/create_audit_request_body.py +43 -0
  172. kard/transactions/types/create_audit_request_data_union.py +26 -0
  173. kard/transactions/types/create_audit_response_body.py +41 -0
  174. kard/transactions/types/create_audit_response_data_union.py +27 -0
  175. kard/transactions/types/direction_type.py +5 -0
  176. kard/transactions/types/fraudulent_transaction_attributes.py +24 -0
  177. kard/transactions/types/fraudulent_transaction_data.py +30 -0
  178. kard/transactions/types/fraudulent_transaction_object.py +42 -0
  179. kard/transactions/types/fraudulent_transaction_request_body.py +45 -0
  180. kard/transactions/types/fraudulent_transaction_response.py +21 -0
  181. kard/transactions/types/get_earned_rewards_response.py +133 -0
  182. kard/transactions/types/matched_transactions_attributes.py +156 -0
  183. kard/transactions/types/matched_transactions_request.py +25 -0
  184. kard/transactions/types/merchant.py +82 -0
  185. kard/transactions/types/payment_status.py +5 -0
  186. kard/transactions/types/payment_type.py +5 -0
  187. kard/transactions/types/processor_mid.py +26 -0
  188. kard/transactions/types/receipt_medium_type.py +5 -0
  189. kard/transactions/types/rewarded_transaction.py +27 -0
  190. kard/transactions/types/rewarded_transaction_attributes.py +74 -0
  191. kard/transactions/types/rewarded_transaction_relationships.py +22 -0
  192. kard/transactions/types/rewarded_transaction_status.py +5 -0
  193. kard/transactions/types/rewarded_transaction_union.py +29 -0
  194. kard/transactions/types/states.py +68 -0
  195. kard/transactions/types/transaction_included_resource.py +47 -0
  196. kard/transactions/types/transaction_merchant_attributes.py +22 -0
  197. kard/transactions/types/transaction_merchant_resource.py +28 -0
  198. kard/transactions/types/transaction_offer_attributes.py +26 -0
  199. kard/transactions/types/transaction_offer_resource.py +28 -0
  200. kard/transactions/types/transaction_payment_type.py +5 -0
  201. kard/transactions/types/transaction_status.py +5 -0
  202. kard/transactions/types/transactions.py +46 -0
  203. kard/transactions/types/transactions_attributes.py +198 -0
  204. kard/transactions/types/transactions_multi_response.py +21 -0
  205. kard/transactions/types/transactions_request.py +25 -0
  206. kard/transactions/types/transactions_request_body.py +90 -0
  207. kard/transactions/types/transactions_response.py +38 -0
  208. kard/transactions/types/transactions_response_data.py +27 -0
  209. kard/transactions/types/visa_mid.py +23 -0
  210. kard/transactions/types/visa_mid_details.py +27 -0
  211. kard/users/__init__.py +293 -0
  212. kard/users/attributions/__init__.py +73 -0
  213. kard/users/attributions/client.py +229 -0
  214. kard/users/attributions/raw_client.py +215 -0
  215. kard/users/attributions/types/__init__.py +73 -0
  216. kard/users/attributions/types/create_attribution_request_object.py +75 -0
  217. kard/users/attributions/types/create_attribution_request_union.py +45 -0
  218. kard/users/attributions/types/create_attribution_response.py +38 -0
  219. kard/users/attributions/types/event_code.py +5 -0
  220. kard/users/attributions/types/notification_attribution_attributes.py +35 -0
  221. kard/users/attributions/types/notification_attribution_request.py +20 -0
  222. kard/users/attributions/types/notification_medium.py +5 -0
  223. kard/users/attributions/types/offer_attribution_attributes.py +35 -0
  224. kard/users/attributions/types/offer_attribution_request.py +20 -0
  225. kard/users/attributions/types/offer_medium.py +5 -0
  226. kard/users/client.py +512 -0
  227. kard/users/errors/__init__.py +34 -0
  228. kard/users/errors/multi_status.py +11 -0
  229. kard/users/raw_client.py +783 -0
  230. kard/users/rewards/__init__.py +133 -0
  231. kard/users/rewards/client.py +448 -0
  232. kard/users/rewards/raw_client.py +587 -0
  233. kard/users/rewards/types/__init__.py +130 -0
  234. kard/users/rewards/types/amount.py +21 -0
  235. kard/users/rewards/types/amount_type.py +5 -0
  236. kard/users/rewards/types/asset.py +28 -0
  237. kard/users/rewards/types/category_data.py +18 -0
  238. kard/users/rewards/types/category_fields.py +23 -0
  239. kard/users/rewards/types/category_identifier.py +24 -0
  240. kard/users/rewards/types/category_included.py +21 -0
  241. kard/users/rewards/types/category_relationship.py +20 -0
  242. kard/users/rewards/types/category_relationship_object.py +20 -0
  243. kard/users/rewards/types/commission.py +21 -0
  244. kard/users/rewards/types/coordinates.py +20 -0
  245. kard/users/rewards/types/eligibility_location_address.py +24 -0
  246. kard/users/rewards/types/eligibility_location_included.py +8 -0
  247. kard/users/rewards/types/eligibility_offer_included.py +7 -0
  248. kard/users/rewards/types/eligibility_offer_relationship.py +7 -0
  249. kard/users/rewards/types/location_attributes.py +28 -0
  250. kard/users/rewards/types/location_data.py +32 -0
  251. kard/users/rewards/types/location_relationships.py +18 -0
  252. kard/users/rewards/types/location_sort_options.py +5 -0
  253. kard/users/rewards/types/locations_response_object.py +215 -0
  254. kard/users/rewards/types/offer_common_fields.py +103 -0
  255. kard/users/rewards/types/offer_data_union.py +30 -0
  256. kard/users/rewards/types/offer_relationship.py +21 -0
  257. kard/users/rewards/types/offer_sort_options.py +7 -0
  258. kard/users/rewards/types/offers_response_object.py +130 -0
  259. kard/users/rewards/types/operation_hours.py +23 -0
  260. kard/users/rewards/types/operation_period.py +21 -0
  261. kard/users/rewards/types/operation_time.py +20 -0
  262. kard/users/rewards/types/standard_offer.py +21 -0
  263. kard/users/rewards/types/standard_offer_core.py +26 -0
  264. kard/users/rewards/types/standard_offer_fields.py +18 -0
  265. kard/users/types/__init__.py +66 -0
  266. kard/users/types/create_users_multi_status_response.py +21 -0
  267. kard/users/types/create_users_object.py +42 -0
  268. kard/users/types/delete_user_response_object.py +34 -0
  269. kard/users/types/update_user_object.py +40 -0
  270. kard/users/types/user_request_attributes.py +60 -0
  271. kard/users/types/user_request_data.py +22 -0
  272. kard/users/types/user_request_data_union.py +28 -0
  273. kard/users/types/user_response_no_data.py +22 -0
  274. kard/users/types/user_response_union_no_data.py +28 -0
  275. kard/users/uploads/__init__.py +112 -0
  276. kard/users/uploads/client.py +484 -0
  277. kard/users/uploads/errors/__init__.py +34 -0
  278. kard/users/uploads/errors/upload_part_multi_status.py +13 -0
  279. kard/users/uploads/raw_client.py +625 -0
  280. kard/users/uploads/types/__init__.py +119 -0
  281. kard/users/uploads/types/create_upload_part_data_union.py +27 -0
  282. kard/users/uploads/types/create_upload_part_multi_status_response.py +21 -0
  283. kard/users/uploads/types/create_upload_part_request_object.py +74 -0
  284. kard/users/uploads/types/create_upload_part_response_data.py +25 -0
  285. kard/users/uploads/types/create_upload_part_response_data_union.py +27 -0
  286. kard/users/uploads/types/create_upload_part_response_object.py +39 -0
  287. kard/users/uploads/types/create_upload_request_data_union.py +26 -0
  288. kard/users/uploads/types/create_upload_request_object.py +36 -0
  289. kard/users/uploads/types/create_upload_response_data.py +25 -0
  290. kard/users/uploads/types/create_upload_response_data_union.py +27 -0
  291. kard/users/uploads/types/create_upload_response_object.py +37 -0
  292. kard/users/uploads/types/historical_transaction_complete_no_data.py +25 -0
  293. kard/users/uploads/types/start_historical_upload_no_data.py +20 -0
  294. kard/users/uploads/types/update_upload_request_data_union.py +27 -0
  295. kard/users/uploads/types/update_upload_request_object.py +37 -0
  296. kard/users/uploads/types/update_upload_response_data.py +25 -0
  297. kard/users/uploads/types/update_upload_response_data_union.py +27 -0
  298. kard/users/uploads/types/update_upload_response_object.py +37 -0
  299. kard/version.py +3 -0
  300. kard_financial_sdk-0.0.82.dist-info/METADATA +238 -0
  301. kard_financial_sdk-0.0.82.dist-info/RECORD +302 -0
  302. kard_financial_sdk-0.0.82.dist-info/WHEEL +4 -0
@@ -0,0 +1,613 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ import asyncio
4
+ import email.utils
5
+ import re
6
+ import time
7
+ import typing
8
+ import urllib.parse
9
+ from contextlib import asynccontextmanager, contextmanager
10
+ from random import random
11
+
12
+ import httpx
13
+ from .file import File, convert_file_dict_to_httpx_tuples
14
+ from .force_multipart import FORCE_MULTIPART
15
+ from .jsonable_encoder import jsonable_encoder
16
+ from .query_encoder import encode_query
17
+ from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict
18
+ from .request_options import RequestOptions
19
+ from httpx._types import RequestFiles
20
+
21
+ INITIAL_RETRY_DELAY_SECONDS = 1.0
22
+ MAX_RETRY_DELAY_SECONDS = 60.0
23
+ JITTER_FACTOR = 0.2 # 20% random jitter
24
+
25
+
26
+ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
27
+ """
28
+ This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
29
+
30
+ Inspired by the urllib3 retry implementation.
31
+ """
32
+ retry_after_ms = response_headers.get("retry-after-ms")
33
+ if retry_after_ms is not None:
34
+ try:
35
+ return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0
36
+ except Exception:
37
+ pass
38
+
39
+ retry_after = response_headers.get("retry-after")
40
+ if retry_after is None:
41
+ return None
42
+
43
+ # Attempt to parse the header as an int.
44
+ if re.match(r"^\s*[0-9]+\s*$", retry_after):
45
+ seconds = float(retry_after)
46
+ # Fallback to parsing it as a date.
47
+ else:
48
+ retry_date_tuple = email.utils.parsedate_tz(retry_after)
49
+ if retry_date_tuple is None:
50
+ return None
51
+ if retry_date_tuple[9] is None: # Python 2
52
+ # Assume UTC if no timezone was specified
53
+ # On Python2.7, parsedate_tz returns None for a timezone offset
54
+ # instead of 0 if no timezone is given, where mktime_tz treats
55
+ # a None timezone offset as local time.
56
+ retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
57
+
58
+ retry_date = email.utils.mktime_tz(retry_date_tuple)
59
+ seconds = retry_date - time.time()
60
+
61
+ if seconds < 0:
62
+ seconds = 0
63
+
64
+ return seconds
65
+
66
+
67
+ def _add_positive_jitter(delay: float) -> float:
68
+ """Add positive jitter (0-20%) to prevent thundering herd."""
69
+ jitter_multiplier = 1 + random() * JITTER_FACTOR
70
+ return delay * jitter_multiplier
71
+
72
+
73
+ def _add_symmetric_jitter(delay: float) -> float:
74
+ """Add symmetric jitter (±10%) for exponential backoff."""
75
+ jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
76
+ return delay * jitter_multiplier
77
+
78
+
79
+ def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
80
+ """
81
+ Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
82
+ Returns seconds to wait, or None if header is missing/invalid.
83
+ """
84
+ reset_time_str = response_headers.get("x-ratelimit-reset")
85
+ if reset_time_str is None:
86
+ return None
87
+
88
+ try:
89
+ reset_time = int(reset_time_str)
90
+ delay = reset_time - time.time()
91
+ if delay > 0:
92
+ return delay
93
+ except (ValueError, TypeError):
94
+ pass
95
+
96
+ return None
97
+
98
+
99
+ def _retry_timeout(response: httpx.Response, retries: int) -> float:
100
+ """
101
+ Determine the amount of time to wait before retrying a request.
102
+ This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
103
+ with a jitter to determine the number of seconds to wait.
104
+ """
105
+
106
+ # 1. Check Retry-After header first
107
+ retry_after = _parse_retry_after(response.headers)
108
+ if retry_after is not None and retry_after > 0:
109
+ return min(retry_after, MAX_RETRY_DELAY_SECONDS)
110
+
111
+ # 2. Check X-RateLimit-Reset header (with positive jitter)
112
+ ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
113
+ if ratelimit_reset is not None:
114
+ return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
115
+
116
+ # 3. Fall back to exponential backoff (with symmetric jitter)
117
+ backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
118
+ return _add_symmetric_jitter(backoff)
119
+
120
+
121
+ def _should_retry(response: httpx.Response) -> bool:
122
+ retryable_400s = [429, 408, 409]
123
+ return response.status_code >= 500 or response.status_code in retryable_400s
124
+
125
+
126
+ def _maybe_filter_none_from_multipart_data(
127
+ data: typing.Optional[typing.Any],
128
+ request_files: typing.Optional[RequestFiles],
129
+ force_multipart: typing.Optional[bool],
130
+ ) -> typing.Optional[typing.Any]:
131
+ """
132
+ Filter None values from data body for multipart/form requests.
133
+ This prevents httpx from converting None to empty strings in multipart encoding.
134
+ Only applies when files are present or force_multipart is True.
135
+ """
136
+ if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart):
137
+ return remove_none_from_dict(data)
138
+ return data
139
+
140
+
141
+ def remove_omit_from_dict(
142
+ original: typing.Dict[str, typing.Optional[typing.Any]],
143
+ omit: typing.Optional[typing.Any],
144
+ ) -> typing.Dict[str, typing.Any]:
145
+ if omit is None:
146
+ return original
147
+ new: typing.Dict[str, typing.Any] = {}
148
+ for key, value in original.items():
149
+ if value is not omit:
150
+ new[key] = value
151
+ return new
152
+
153
+
154
+ def maybe_filter_request_body(
155
+ data: typing.Optional[typing.Any],
156
+ request_options: typing.Optional[RequestOptions],
157
+ omit: typing.Optional[typing.Any],
158
+ ) -> typing.Optional[typing.Any]:
159
+ if data is None:
160
+ return (
161
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
162
+ if request_options is not None
163
+ else None
164
+ )
165
+ elif not isinstance(data, typing.Mapping):
166
+ data_content = jsonable_encoder(data)
167
+ else:
168
+ data_content = {
169
+ **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore
170
+ **(
171
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
172
+ if request_options is not None
173
+ else {}
174
+ ),
175
+ }
176
+ return data_content
177
+
178
+
179
+ # Abstracted out for testing purposes
180
+ def get_request_body(
181
+ *,
182
+ json: typing.Optional[typing.Any],
183
+ data: typing.Optional[typing.Any],
184
+ request_options: typing.Optional[RequestOptions],
185
+ omit: typing.Optional[typing.Any],
186
+ ) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
187
+ json_body = None
188
+ data_body = None
189
+ if data is not None:
190
+ data_body = maybe_filter_request_body(data, request_options, omit)
191
+ else:
192
+ # If both data and json are None, we send json data in the event extra properties are specified
193
+ json_body = maybe_filter_request_body(json, request_options, omit)
194
+
195
+ # If you have an empty JSON body, you should just send None
196
+ return (json_body if json_body != {} else None), data_body if data_body != {} else None
197
+
198
+
199
+ class HttpClient:
200
+ def __init__(
201
+ self,
202
+ *,
203
+ httpx_client: httpx.Client,
204
+ base_timeout: typing.Callable[[], typing.Optional[float]],
205
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
206
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
207
+ ):
208
+ self.base_url = base_url
209
+ self.base_timeout = base_timeout
210
+ self.base_headers = base_headers
211
+ self.httpx_client = httpx_client
212
+
213
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
214
+ base_url = maybe_base_url
215
+ if self.base_url is not None and base_url is None:
216
+ base_url = self.base_url()
217
+
218
+ if base_url is None:
219
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
220
+ return base_url
221
+
222
+ def request(
223
+ self,
224
+ path: typing.Optional[str] = None,
225
+ *,
226
+ method: str,
227
+ base_url: typing.Optional[str] = None,
228
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
229
+ json: typing.Optional[typing.Any] = None,
230
+ data: typing.Optional[typing.Any] = None,
231
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
232
+ files: typing.Optional[
233
+ typing.Union[
234
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
235
+ typing.List[typing.Tuple[str, File]],
236
+ ]
237
+ ] = None,
238
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
239
+ request_options: typing.Optional[RequestOptions] = None,
240
+ retries: int = 2,
241
+ omit: typing.Optional[typing.Any] = None,
242
+ force_multipart: typing.Optional[bool] = None,
243
+ ) -> httpx.Response:
244
+ base_url = self.get_base_url(base_url)
245
+ timeout = (
246
+ request_options.get("timeout_in_seconds")
247
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
248
+ else self.base_timeout()
249
+ )
250
+
251
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
252
+
253
+ request_files: typing.Optional[RequestFiles] = (
254
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
255
+ if (files is not None and files is not omit and isinstance(files, dict))
256
+ else None
257
+ )
258
+
259
+ if (request_files is None or len(request_files) == 0) and force_multipart:
260
+ request_files = FORCE_MULTIPART
261
+
262
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
263
+
264
+ response = self.httpx_client.request(
265
+ method=method,
266
+ url=urllib.parse.urljoin(f"{base_url}/", path),
267
+ headers=jsonable_encoder(
268
+ remove_none_from_dict(
269
+ {
270
+ **self.base_headers(),
271
+ **(headers if headers is not None else {}),
272
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
273
+ }
274
+ )
275
+ ),
276
+ params=encode_query(
277
+ jsonable_encoder(
278
+ remove_none_from_dict(
279
+ remove_omit_from_dict(
280
+ {
281
+ **(params if params is not None else {}),
282
+ **(
283
+ request_options.get("additional_query_parameters", {}) or {}
284
+ if request_options is not None
285
+ else {}
286
+ ),
287
+ },
288
+ omit,
289
+ )
290
+ )
291
+ )
292
+ ),
293
+ json=json_body,
294
+ data=data_body,
295
+ content=content,
296
+ files=request_files,
297
+ timeout=timeout,
298
+ )
299
+
300
+ max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
301
+ if _should_retry(response=response):
302
+ if max_retries > retries:
303
+ time.sleep(_retry_timeout(response=response, retries=retries))
304
+ return self.request(
305
+ path=path,
306
+ method=method,
307
+ base_url=base_url,
308
+ params=params,
309
+ json=json,
310
+ content=content,
311
+ files=files,
312
+ headers=headers,
313
+ request_options=request_options,
314
+ retries=retries + 1,
315
+ omit=omit,
316
+ )
317
+
318
+ return response
319
+
320
+ @contextmanager
321
+ def stream(
322
+ self,
323
+ path: typing.Optional[str] = None,
324
+ *,
325
+ method: str,
326
+ base_url: typing.Optional[str] = None,
327
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
328
+ json: typing.Optional[typing.Any] = None,
329
+ data: typing.Optional[typing.Any] = None,
330
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
331
+ files: typing.Optional[
332
+ typing.Union[
333
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
334
+ typing.List[typing.Tuple[str, File]],
335
+ ]
336
+ ] = None,
337
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
338
+ request_options: typing.Optional[RequestOptions] = None,
339
+ retries: int = 2,
340
+ omit: typing.Optional[typing.Any] = None,
341
+ force_multipart: typing.Optional[bool] = None,
342
+ ) -> typing.Iterator[httpx.Response]:
343
+ base_url = self.get_base_url(base_url)
344
+ timeout = (
345
+ request_options.get("timeout_in_seconds")
346
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
347
+ else self.base_timeout()
348
+ )
349
+
350
+ request_files: typing.Optional[RequestFiles] = (
351
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
352
+ if (files is not None and files is not omit and isinstance(files, dict))
353
+ else None
354
+ )
355
+
356
+ if (request_files is None or len(request_files) == 0) and force_multipart:
357
+ request_files = FORCE_MULTIPART
358
+
359
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
360
+
361
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
362
+
363
+ with self.httpx_client.stream(
364
+ method=method,
365
+ url=urllib.parse.urljoin(f"{base_url}/", path),
366
+ headers=jsonable_encoder(
367
+ remove_none_from_dict(
368
+ {
369
+ **self.base_headers(),
370
+ **(headers if headers is not None else {}),
371
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
372
+ }
373
+ )
374
+ ),
375
+ params=encode_query(
376
+ jsonable_encoder(
377
+ remove_none_from_dict(
378
+ remove_omit_from_dict(
379
+ {
380
+ **(params if params is not None else {}),
381
+ **(
382
+ request_options.get("additional_query_parameters", {})
383
+ if request_options is not None
384
+ else {}
385
+ ),
386
+ },
387
+ omit,
388
+ )
389
+ )
390
+ )
391
+ ),
392
+ json=json_body,
393
+ data=data_body,
394
+ content=content,
395
+ files=request_files,
396
+ timeout=timeout,
397
+ ) as stream:
398
+ yield stream
399
+
400
+
401
+ class AsyncHttpClient:
402
+ def __init__(
403
+ self,
404
+ *,
405
+ httpx_client: httpx.AsyncClient,
406
+ base_timeout: typing.Callable[[], typing.Optional[float]],
407
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
408
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
409
+ async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None,
410
+ ):
411
+ self.base_url = base_url
412
+ self.base_timeout = base_timeout
413
+ self.base_headers = base_headers
414
+ self.async_base_headers = async_base_headers
415
+ self.httpx_client = httpx_client
416
+
417
+ async def _get_headers(self) -> typing.Dict[str, str]:
418
+ if self.async_base_headers is not None:
419
+ return await self.async_base_headers()
420
+ return self.base_headers()
421
+
422
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
423
+ base_url = maybe_base_url
424
+ if self.base_url is not None and base_url is None:
425
+ base_url = self.base_url()
426
+
427
+ if base_url is None:
428
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
429
+ return base_url
430
+
431
+ async def request(
432
+ self,
433
+ path: typing.Optional[str] = None,
434
+ *,
435
+ method: str,
436
+ base_url: typing.Optional[str] = None,
437
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
438
+ json: typing.Optional[typing.Any] = None,
439
+ data: typing.Optional[typing.Any] = None,
440
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
441
+ files: typing.Optional[
442
+ typing.Union[
443
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
444
+ typing.List[typing.Tuple[str, File]],
445
+ ]
446
+ ] = None,
447
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
448
+ request_options: typing.Optional[RequestOptions] = None,
449
+ retries: int = 2,
450
+ omit: typing.Optional[typing.Any] = None,
451
+ force_multipart: typing.Optional[bool] = None,
452
+ ) -> httpx.Response:
453
+ base_url = self.get_base_url(base_url)
454
+ timeout = (
455
+ request_options.get("timeout_in_seconds")
456
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
457
+ else self.base_timeout()
458
+ )
459
+
460
+ request_files: typing.Optional[RequestFiles] = (
461
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
462
+ if (files is not None and files is not omit and isinstance(files, dict))
463
+ else None
464
+ )
465
+
466
+ if (request_files is None or len(request_files) == 0) and force_multipart:
467
+ request_files = FORCE_MULTIPART
468
+
469
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
470
+
471
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
472
+
473
+ # Get headers (supports async token providers)
474
+ _headers = await self._get_headers()
475
+
476
+ # Add the input to each of these and do None-safety checks
477
+ response = await self.httpx_client.request(
478
+ method=method,
479
+ url=urllib.parse.urljoin(f"{base_url}/", path),
480
+ headers=jsonable_encoder(
481
+ remove_none_from_dict(
482
+ {
483
+ **_headers,
484
+ **(headers if headers is not None else {}),
485
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
486
+ }
487
+ )
488
+ ),
489
+ params=encode_query(
490
+ jsonable_encoder(
491
+ remove_none_from_dict(
492
+ remove_omit_from_dict(
493
+ {
494
+ **(params if params is not None else {}),
495
+ **(
496
+ request_options.get("additional_query_parameters", {}) or {}
497
+ if request_options is not None
498
+ else {}
499
+ ),
500
+ },
501
+ omit,
502
+ )
503
+ )
504
+ )
505
+ ),
506
+ json=json_body,
507
+ data=data_body,
508
+ content=content,
509
+ files=request_files,
510
+ timeout=timeout,
511
+ )
512
+
513
+ max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
514
+ if _should_retry(response=response):
515
+ if max_retries > retries:
516
+ await asyncio.sleep(_retry_timeout(response=response, retries=retries))
517
+ return await self.request(
518
+ path=path,
519
+ method=method,
520
+ base_url=base_url,
521
+ params=params,
522
+ json=json,
523
+ content=content,
524
+ files=files,
525
+ headers=headers,
526
+ request_options=request_options,
527
+ retries=retries + 1,
528
+ omit=omit,
529
+ )
530
+ return response
531
+
532
+ @asynccontextmanager
533
+ async def stream(
534
+ self,
535
+ path: typing.Optional[str] = None,
536
+ *,
537
+ method: str,
538
+ base_url: typing.Optional[str] = None,
539
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
540
+ json: typing.Optional[typing.Any] = None,
541
+ data: typing.Optional[typing.Any] = None,
542
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
543
+ files: typing.Optional[
544
+ typing.Union[
545
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
546
+ typing.List[typing.Tuple[str, File]],
547
+ ]
548
+ ] = None,
549
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
550
+ request_options: typing.Optional[RequestOptions] = None,
551
+ retries: int = 2,
552
+ omit: typing.Optional[typing.Any] = None,
553
+ force_multipart: typing.Optional[bool] = None,
554
+ ) -> typing.AsyncIterator[httpx.Response]:
555
+ base_url = self.get_base_url(base_url)
556
+ timeout = (
557
+ request_options.get("timeout_in_seconds")
558
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
559
+ else self.base_timeout()
560
+ )
561
+
562
+ request_files: typing.Optional[RequestFiles] = (
563
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
564
+ if (files is not None and files is not omit and isinstance(files, dict))
565
+ else None
566
+ )
567
+
568
+ if (request_files is None or len(request_files) == 0) and force_multipart:
569
+ request_files = FORCE_MULTIPART
570
+
571
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
572
+
573
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
574
+
575
+ # Get headers (supports async token providers)
576
+ _headers = await self._get_headers()
577
+
578
+ async with self.httpx_client.stream(
579
+ method=method,
580
+ url=urllib.parse.urljoin(f"{base_url}/", path),
581
+ headers=jsonable_encoder(
582
+ remove_none_from_dict(
583
+ {
584
+ **_headers,
585
+ **(headers if headers is not None else {}),
586
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
587
+ }
588
+ )
589
+ ),
590
+ params=encode_query(
591
+ jsonable_encoder(
592
+ remove_none_from_dict(
593
+ remove_omit_from_dict(
594
+ {
595
+ **(params if params is not None else {}),
596
+ **(
597
+ request_options.get("additional_query_parameters", {})
598
+ if request_options is not None
599
+ else {}
600
+ ),
601
+ },
602
+ omit=omit,
603
+ )
604
+ )
605
+ )
606
+ ),
607
+ json=json_body,
608
+ data=data_body,
609
+ content=content,
610
+ files=request_files,
611
+ timeout=timeout,
612
+ ) as stream:
613
+ yield stream
@@ -0,0 +1,55 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ from typing import Dict, Generic, TypeVar
4
+
5
+ import httpx
6
+
7
+ # Generic to represent the underlying type of the data wrapped by the HTTP response.
8
+ T = TypeVar("T")
9
+
10
+
11
+ class BaseHttpResponse:
12
+ """Minimalist HTTP response wrapper that exposes response headers."""
13
+
14
+ _response: httpx.Response
15
+
16
+ def __init__(self, response: httpx.Response):
17
+ self._response = response
18
+
19
+ @property
20
+ def headers(self) -> Dict[str, str]:
21
+ return dict(self._response.headers)
22
+
23
+
24
+ class HttpResponse(Generic[T], BaseHttpResponse):
25
+ """HTTP response wrapper that exposes response headers and data."""
26
+
27
+ _data: T
28
+
29
+ def __init__(self, response: httpx.Response, data: T):
30
+ super().__init__(response)
31
+ self._data = data
32
+
33
+ @property
34
+ def data(self) -> T:
35
+ return self._data
36
+
37
+ def close(self) -> None:
38
+ self._response.close()
39
+
40
+
41
+ class AsyncHttpResponse(Generic[T], BaseHttpResponse):
42
+ """HTTP response wrapper that exposes response headers and data."""
43
+
44
+ _data: T
45
+
46
+ def __init__(self, response: httpx.Response, data: T):
47
+ super().__init__(response)
48
+ self._data = data
49
+
50
+ @property
51
+ def data(self) -> T:
52
+ return self._data
53
+
54
+ async def close(self) -> None:
55
+ await self._response.aclose()