gr4vy 1.1.29__py3-none-any.whl → 1.10.9__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 (219) hide show
  1. gr4vy/_version.py +3 -3
  2. gr4vy/account_updater.py +6 -3
  3. gr4vy/actions.py +307 -0
  4. gr4vy/all.py +4 -2
  5. gr4vy/audit_logs.py +6 -4
  6. gr4vy/balances.py +4 -2
  7. gr4vy/basesdk.py +17 -1
  8. gr4vy/buyers_gift_cards.py +4 -2
  9. gr4vy/buyers_payment_methods.py +4 -2
  10. gr4vy/buyers_sdk.py +43 -25
  11. gr4vy/buyers_shipping_details.py +26 -16
  12. gr4vy/card_scheme_definitions_sdk.py +4 -2
  13. gr4vy/checkout_sessions.py +40 -14
  14. gr4vy/digital_wallets_sdk.py +32 -20
  15. gr4vy/domains.py +14 -10
  16. gr4vy/errors/__init__.py +15 -3
  17. gr4vy/errors/apierror.py +2 -0
  18. gr4vy/errors/error400.py +4 -6
  19. gr4vy/errors/error401.py +4 -6
  20. gr4vy/errors/error403.py +4 -6
  21. gr4vy/errors/error404.py +4 -6
  22. gr4vy/errors/error405.py +4 -6
  23. gr4vy/errors/error409.py +4 -6
  24. gr4vy/errors/error425.py +4 -6
  25. gr4vy/errors/error429.py +4 -6
  26. gr4vy/errors/error500.py +4 -6
  27. gr4vy/errors/error502.py +4 -6
  28. gr4vy/errors/error504.py +4 -6
  29. gr4vy/errors/gr4vyerror.py +11 -7
  30. gr4vy/errors/httpvalidationerror.py +4 -2
  31. gr4vy/errors/no_response_error.py +5 -1
  32. gr4vy/errors/responsevalidationerror.py +2 -0
  33. gr4vy/events.py +54 -7
  34. gr4vy/executions.py +38 -8
  35. gr4vy/gift_cards_sdk.py +29 -19
  36. gr4vy/httpclient.py +0 -1
  37. gr4vy/jobs.py +4 -2
  38. gr4vy/merchant_accounts_sdk.py +30 -10
  39. gr4vy/models/__init__.py +184 -30
  40. gr4vy/models/adyenautorescuesepascenariosenum.py +3 -1
  41. gr4vy/models/adyencardoptions.py +9 -6
  42. gr4vy/models/adyensepaoptions.py +1 -6
  43. gr4vy/models/adyensplitsoptions.py +57 -0
  44. gr4vy/models/airline.py +9 -8
  45. gr4vy/models/airlineleg.py +9 -7
  46. gr4vy/models/airlinepassenger.py +9 -7
  47. gr4vy/models/antifrauddecision.py +9 -1
  48. gr4vy/models/approvaltarget.py +7 -1
  49. gr4vy/models/auditlogaction.py +9 -1
  50. gr4vy/models/auditlogentry.py +3 -3
  51. gr4vy/models/auditlogentryuser.py +3 -3
  52. gr4vy/models/{billingdetails_input.py → billingdetails.py} +2 -2
  53. gr4vy/models/braintreedynamicdatafieldsoptions.py +33 -3
  54. gr4vy/models/browserinfo.py +9 -5
  55. gr4vy/models/buyer.py +3 -3
  56. gr4vy/models/buyercreate.py +3 -3
  57. gr4vy/models/buyerupdate.py +3 -3
  58. gr4vy/models/cancel_transactionop.py +43 -0
  59. gr4vy/models/cancelstatus.py +15 -0
  60. gr4vy/models/capturestatus.py +7 -1
  61. gr4vy/models/cardtype.py +8 -1
  62. gr4vy/models/cartitem.py +53 -8
  63. gr4vy/models/chaseoptions.py +46 -0
  64. gr4vy/models/checkoutsession.py +24 -3
  65. gr4vy/models/checkoutsessioncreate.py +39 -5
  66. gr4vy/models/checkoutsessionpaymentmethod.py +3 -5
  67. gr4vy/models/checkoutsessionpaymentmethoddetails.py +2 -6
  68. gr4vy/models/create_payment_methodop.py +6 -0
  69. gr4vy/models/create_report_execution_urlop.py +16 -1
  70. gr4vy/models/createsession.py +3 -3
  71. gr4vy/models/createsessionstatus.py +7 -1
  72. gr4vy/models/cvvresponsecode.py +7 -1
  73. gr4vy/models/cybersourceoptions.py +7 -0
  74. gr4vy/models/definitionfield.py +1 -6
  75. gr4vy/models/definitionfieldformat.py +8 -1
  76. gr4vy/models/digitalwallet.py +3 -5
  77. gr4vy/models/digitalwalletcreate.py +2 -6
  78. gr4vy/models/digitalwalletprovider.py +6 -1
  79. gr4vy/models/dlocalupioptions.py +50 -0
  80. gr4vy/models/dlocalupirecurringinfooptions.py +47 -0
  81. gr4vy/models/dlocalupiwalletoptions.py +89 -0
  82. gr4vy/models/errordetail.py +2 -4
  83. gr4vy/models/errorlocation.py +8 -1
  84. gr4vy/models/flow.py +16 -0
  85. gr4vy/models/flowaction.py +16 -0
  86. gr4vy/models/forterantifraudoptions.py +8 -8
  87. gr4vy/models/forterantifraudoptionscartitembasicitemdata.py +10 -8
  88. gr4vy/models/forterantifraudoptionscartitemdeliverydetails.py +9 -7
  89. gr4vy/models/giftcardredemption.py +4 -8
  90. gr4vy/models/giftcardredemptionstatus.py +7 -1
  91. gr4vy/models/giftcardservice.py +3 -5
  92. gr4vy/models/giftcardserviceprovider.py +5 -1
  93. gr4vy/models/giftcardsummary.py +3 -5
  94. gr4vy/models/{guestbuyer_input.py → guestbuyer.py} +5 -5
  95. gr4vy/models/instrumenttype.py +1 -0
  96. gr4vy/models/integrationclient.py +16 -0
  97. gr4vy/models/internal/__init__.py +14 -1
  98. gr4vy/models/list_all_report_executionsop.py +2 -14
  99. gr4vy/models/list_audit_logsop.py +2 -10
  100. gr4vy/models/list_buyer_payment_methodsop.py +9 -4
  101. gr4vy/models/list_payment_methodsop.py +2 -14
  102. gr4vy/models/list_payment_servicesop.py +2 -8
  103. gr4vy/models/list_reportsop.py +2 -10
  104. gr4vy/models/list_transaction_actionsop.py +43 -0
  105. gr4vy/models/list_transaction_eventsop.py +12 -1
  106. gr4vy/models/list_transactionsop.py +14 -22
  107. gr4vy/models/merchantaccount.py +9 -5
  108. gr4vy/models/merchantaccountcreate.py +8 -6
  109. gr4vy/models/merchantaccountupdate.py +8 -6
  110. gr4vy/models/merchantprofilescheme.py +7 -7
  111. gr4vy/models/merchantprofileschemesummary.py +7 -7
  112. gr4vy/models/method.py +4 -0
  113. gr4vy/models/mockcardoptions.py +7 -2
  114. gr4vy/models/mode.py +1 -0
  115. gr4vy/models/monatospeioptions.py +15 -0
  116. gr4vy/models/networktoken.py +3 -3
  117. gr4vy/models/networktokenpaymentmethodcreate.py +11 -9
  118. gr4vy/models/networktokenstatus.py +7 -1
  119. gr4vy/models/nuveiidealoptions.py +49 -0
  120. gr4vy/models/nuveipseoptions.py +70 -0
  121. gr4vy/models/paymentlink.py +25 -7
  122. gr4vy/models/paymentlinkcreate.py +23 -28
  123. gr4vy/models/paymentlinkstatus.py +7 -1
  124. gr4vy/models/paymentmethod.py +7 -13
  125. gr4vy/models/paymentmethodcard.py +3 -5
  126. gr4vy/models/paymentmethoddetailscard.py +2 -6
  127. gr4vy/models/paymentmethodstatus.py +7 -1
  128. gr4vy/models/paymentmethodsummary.py +6 -12
  129. gr4vy/models/paymentoption.py +3 -3
  130. gr4vy/models/paymentservice.py +4 -6
  131. gr4vy/models/paymentserviceconfiguration.py +2 -6
  132. gr4vy/models/paymentservicedefinition.py +11 -5
  133. gr4vy/models/paymentservicestatus.py +8 -1
  134. gr4vy/models/paymentservicetoken.py +3 -3
  135. gr4vy/models/payoutcategory.py +1 -1
  136. gr4vy/models/payoutcreate.py +5 -9
  137. gr4vy/models/payoutstatus.py +7 -1
  138. gr4vy/models/payoutsummary.py +4 -6
  139. gr4vy/models/paypaloptions.py +8 -2
  140. gr4vy/models/paypalshippingoptions.py +51 -0
  141. gr4vy/models/paypalshippingoptionsitem.py +89 -0
  142. gr4vy/models/paypalshippingoptionsitemamount.py +20 -0
  143. gr4vy/models/plaidpaymentmethodcreate.py +106 -0
  144. gr4vy/models/redirectpaymentmethodcreate.py +4 -6
  145. gr4vy/models/refund.py +4 -4
  146. gr4vy/models/refundstatus.py +8 -1
  147. gr4vy/models/refundtargettype.py +5 -1
  148. gr4vy/models/report.py +4 -6
  149. gr4vy/models/reportcreate.py +2 -3
  150. gr4vy/models/reportcreatortype.py +7 -1
  151. gr4vy/models/reportexecution.py +3 -3
  152. gr4vy/models/reportexecutionstatus.py +7 -1
  153. gr4vy/models/reportexecutionsummary.py +3 -3
  154. gr4vy/models/reportexecutionurlgenerate.py +16 -0
  155. gr4vy/models/reportschedule.py +9 -1
  156. gr4vy/models/reportspec.py +2 -4
  157. gr4vy/models/reportsummary.py +3 -5
  158. gr4vy/models/settlement.py +32 -32
  159. gr4vy/models/statementdescriptor.py +7 -0
  160. gr4vy/models/stripeconnectoptions.py +7 -0
  161. gr4vy/models/taxid.py +2 -4
  162. gr4vy/models/taxidkind.py +7 -0
  163. gr4vy/models/threedsecuredatav1.py +2 -6
  164. gr4vy/models/threedsecuredatav2.py +2 -6
  165. gr4vy/models/threedsecuremethod.py +7 -1
  166. gr4vy/models/threedsecurestatus.py +7 -1
  167. gr4vy/models/transaction.py +86 -25
  168. gr4vy/models/transactionaction.py +48 -0
  169. gr4vy/models/transactionactions.py +17 -0
  170. gr4vy/models/transactionbuyer.py +3 -3
  171. gr4vy/models/transactioncancel.py +81 -0
  172. gr4vy/models/transactioncapture.py +3 -3
  173. gr4vy/models/transactionconnectionoptions.py +50 -0
  174. gr4vy/models/transactioncreate.py +76 -12
  175. gr4vy/models/transactionevent.py +10 -3
  176. gr4vy/models/transactionintent.py +7 -1
  177. gr4vy/models/transactionintentoutcome.py +6 -1
  178. gr4vy/models/transactionpaymentmethod.py +6 -12
  179. gr4vy/models/transactionpaymentservice.py +3 -3
  180. gr4vy/models/transactionpaymentsource.py +7 -1
  181. gr4vy/models/transactionrefundcreate.py +2 -6
  182. gr4vy/models/transactionsummary.py +11 -10
  183. gr4vy/models/transactionthreedsecuresummary.py +3 -9
  184. gr4vy/models/transactionvoid.py +3 -3
  185. gr4vy/models/userstatus.py +8 -1
  186. gr4vy/models/voidstatus.py +7 -1
  187. gr4vy/network_tokens_cryptogram.py +4 -2
  188. gr4vy/payment_links_sdk.py +36 -16
  189. gr4vy/payment_methods_network_tokens.py +27 -13
  190. gr4vy/payment_methods_payment_service_tokens.py +12 -6
  191. gr4vy/payment_methods_sdk.py +26 -14
  192. gr4vy/payment_options_sdk.py +4 -2
  193. gr4vy/payment_service_definitions_sdk.py +14 -8
  194. gr4vy/payment_services_sdk.py +36 -22
  195. gr4vy/payouts.py +18 -12
  196. gr4vy/refunds_sdk.py +4 -2
  197. gr4vy/report_executions_sdk.py +6 -4
  198. gr4vy/reports_sdk.py +23 -13
  199. gr4vy/sdk.py +17 -4
  200. gr4vy/sessions.py +12 -6
  201. gr4vy/transactions.py +446 -33
  202. gr4vy/transactions_refunds.py +17 -9
  203. gr4vy/transactions_settlements.py +8 -4
  204. gr4vy/types/basemodel.py +41 -3
  205. gr4vy/utils/__init__.py +15 -6
  206. gr4vy/utils/annotations.py +32 -8
  207. gr4vy/utils/enums.py +60 -0
  208. gr4vy/utils/eventstreaming.py +10 -0
  209. gr4vy/utils/forms.py +21 -10
  210. gr4vy/utils/queryparams.py +14 -2
  211. gr4vy/utils/requestbodies.py +1 -1
  212. gr4vy/utils/retries.py +69 -5
  213. gr4vy/utils/serializers.py +0 -20
  214. gr4vy/utils/unmarshal_json_response.py +15 -1
  215. {gr4vy-1.1.29.dist-info → gr4vy-1.10.9.dist-info}/METADATA +43 -41
  216. {gr4vy-1.1.29.dist-info → gr4vy-1.10.9.dist-info}/RECORD +217 -196
  217. {gr4vy-1.1.29.dist-info → gr4vy-1.10.9.dist-info}/WHEEL +1 -1
  218. gr4vy/models/billingdetails_output.py +0 -87
  219. gr4vy/models/guestbuyer_output.py +0 -80
@@ -14,13 +14,15 @@ from typing import Any, Mapping, Optional
14
14
  class TransactionsRefunds(BaseSDK):
15
15
  all: All
16
16
 
17
- def __init__(self, sdk_config: SDKConfiguration) -> None:
18
- BaseSDK.__init__(self, sdk_config)
17
+ def __init__(
18
+ self, sdk_config: SDKConfiguration, parent_ref: Optional[object] = None
19
+ ) -> None:
20
+ BaseSDK.__init__(self, sdk_config, parent_ref=parent_ref)
19
21
  self.sdk_configuration = sdk_config
20
22
  self._init_sdks()
21
23
 
22
24
  def _init_sdks(self):
23
- self.all = All(self.sdk_configuration)
25
+ self.all = All(self.sdk_configuration, parent_ref=self.parent_ref)
24
26
 
25
27
  def list(
26
28
  self,
@@ -74,6 +76,7 @@ class TransactionsRefunds(BaseSDK):
74
76
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
75
77
  ),
76
78
  security=self.sdk_configuration.security,
79
+ allow_empty_value=None,
77
80
  timeout_ms=timeout_ms,
78
81
  )
79
82
 
@@ -94,7 +97,7 @@ class TransactionsRefunds(BaseSDK):
94
97
  config=self.sdk_configuration,
95
98
  base_url=base_url or "",
96
99
  operation_id="list_transaction_refunds",
97
- oauth2_scopes=[],
100
+ oauth2_scopes=None,
98
101
  security_source=get_security_from_env(
99
102
  self.sdk_configuration.security, models.Security
100
103
  ),
@@ -221,6 +224,7 @@ class TransactionsRefunds(BaseSDK):
221
224
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
222
225
  ),
223
226
  security=self.sdk_configuration.security,
227
+ allow_empty_value=None,
224
228
  timeout_ms=timeout_ms,
225
229
  )
226
230
 
@@ -241,7 +245,7 @@ class TransactionsRefunds(BaseSDK):
241
245
  config=self.sdk_configuration,
242
246
  base_url=base_url or "",
243
247
  operation_id="list_transaction_refunds",
244
- oauth2_scopes=[],
248
+ oauth2_scopes=None,
245
249
  security_source=get_security_from_env(
246
250
  self.sdk_configuration.security, models.Security
247
251
  ),
@@ -392,6 +396,7 @@ class TransactionsRefunds(BaseSDK):
392
396
  "json",
393
397
  models.TransactionRefundCreate,
394
398
  ),
399
+ allow_empty_value=None,
395
400
  timeout_ms=timeout_ms,
396
401
  )
397
402
 
@@ -408,7 +413,7 @@ class TransactionsRefunds(BaseSDK):
408
413
  config=self.sdk_configuration,
409
414
  base_url=base_url or "",
410
415
  operation_id="create_transaction_refund",
411
- oauth2_scopes=[],
416
+ oauth2_scopes=None,
412
417
  security_source=get_security_from_env(
413
418
  self.sdk_configuration.security, models.Security
414
419
  ),
@@ -559,6 +564,7 @@ class TransactionsRefunds(BaseSDK):
559
564
  "json",
560
565
  models.TransactionRefundCreate,
561
566
  ),
567
+ allow_empty_value=None,
562
568
  timeout_ms=timeout_ms,
563
569
  )
564
570
 
@@ -575,7 +581,7 @@ class TransactionsRefunds(BaseSDK):
575
581
  config=self.sdk_configuration,
576
582
  base_url=base_url or "",
577
583
  operation_id="create_transaction_refund",
578
- oauth2_scopes=[],
584
+ oauth2_scopes=None,
579
585
  security_source=get_security_from_env(
580
586
  self.sdk_configuration.security, models.Security
581
587
  ),
@@ -705,6 +711,7 @@ class TransactionsRefunds(BaseSDK):
705
711
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
706
712
  ),
707
713
  security=self.sdk_configuration.security,
714
+ allow_empty_value=None,
708
715
  timeout_ms=timeout_ms,
709
716
  )
710
717
 
@@ -725,7 +732,7 @@ class TransactionsRefunds(BaseSDK):
725
732
  config=self.sdk_configuration,
726
733
  base_url=base_url or "",
727
734
  operation_id="get_transaction_refund",
728
- oauth2_scopes=[],
735
+ oauth2_scopes=None,
729
736
  security_source=get_security_from_env(
730
737
  self.sdk_configuration.security, models.Security
731
738
  ),
@@ -855,6 +862,7 @@ class TransactionsRefunds(BaseSDK):
855
862
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
856
863
  ),
857
864
  security=self.sdk_configuration.security,
865
+ allow_empty_value=None,
858
866
  timeout_ms=timeout_ms,
859
867
  )
860
868
 
@@ -875,7 +883,7 @@ class TransactionsRefunds(BaseSDK):
875
883
  config=self.sdk_configuration,
876
884
  base_url=base_url or "",
877
885
  operation_id="get_transaction_refund",
878
- oauth2_scopes=[],
886
+ oauth2_scopes=None,
879
887
  security_source=get_security_from_env(
880
888
  self.sdk_configuration.security, models.Security
881
889
  ),
@@ -65,6 +65,7 @@ class TransactionsSettlements(BaseSDK):
65
65
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
66
66
  ),
67
67
  security=self.sdk_configuration.security,
68
+ allow_empty_value=None,
68
69
  timeout_ms=timeout_ms,
69
70
  )
70
71
 
@@ -85,7 +86,7 @@ class TransactionsSettlements(BaseSDK):
85
86
  config=self.sdk_configuration,
86
87
  base_url=base_url or "",
87
88
  operation_id="get_transaction_settlement",
88
- oauth2_scopes=[],
89
+ oauth2_scopes=None,
89
90
  security_source=get_security_from_env(
90
91
  self.sdk_configuration.security, models.Security
91
92
  ),
@@ -215,6 +216,7 @@ class TransactionsSettlements(BaseSDK):
215
216
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
216
217
  ),
217
218
  security=self.sdk_configuration.security,
219
+ allow_empty_value=None,
218
220
  timeout_ms=timeout_ms,
219
221
  )
220
222
 
@@ -235,7 +237,7 @@ class TransactionsSettlements(BaseSDK):
235
237
  config=self.sdk_configuration,
236
238
  base_url=base_url or "",
237
239
  operation_id="get_transaction_settlement",
238
- oauth2_scopes=[],
240
+ oauth2_scopes=None,
239
241
  security_source=get_security_from_env(
240
242
  self.sdk_configuration.security, models.Security
241
243
  ),
@@ -362,6 +364,7 @@ class TransactionsSettlements(BaseSDK):
362
364
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
363
365
  ),
364
366
  security=self.sdk_configuration.security,
367
+ allow_empty_value=None,
365
368
  timeout_ms=timeout_ms,
366
369
  )
367
370
 
@@ -382,7 +385,7 @@ class TransactionsSettlements(BaseSDK):
382
385
  config=self.sdk_configuration,
383
386
  base_url=base_url or "",
384
387
  operation_id="list_transaction_settlements",
385
- oauth2_scopes=[],
388
+ oauth2_scopes=None,
386
389
  security_source=get_security_from_env(
387
390
  self.sdk_configuration.security, models.Security
388
391
  ),
@@ -509,6 +512,7 @@ class TransactionsSettlements(BaseSDK):
509
512
  merchant_account_id=self.sdk_configuration.globals.merchant_account_id,
510
513
  ),
511
514
  security=self.sdk_configuration.security,
515
+ allow_empty_value=None,
512
516
  timeout_ms=timeout_ms,
513
517
  )
514
518
 
@@ -529,7 +533,7 @@ class TransactionsSettlements(BaseSDK):
529
533
  config=self.sdk_configuration,
530
534
  base_url=base_url or "",
531
535
  operation_id="list_transaction_settlements",
532
- oauth2_scopes=[],
536
+ oauth2_scopes=None,
533
537
  security_source=get_security_from_env(
534
538
  self.sdk_configuration.security, models.Security
535
539
  ),
gr4vy/types/basemodel.py CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  from pydantic import ConfigDict, model_serializer
4
4
  from pydantic import BaseModel as PydanticBaseModel
5
- from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union
5
+ from pydantic_core import core_schema
6
+ from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
6
7
  from typing_extensions import TypeAliasType, TypeAlias
7
8
 
8
9
 
@@ -35,5 +36,42 @@ else:
35
36
  "OptionalNullable", Union[Optional[Nullable[T]], Unset], type_params=(T,)
36
37
  )
37
38
 
38
- UnrecognizedInt: TypeAlias = int
39
- UnrecognizedStr: TypeAlias = str
39
+
40
+ class UnrecognizedStr(str):
41
+ @classmethod
42
+ def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> core_schema.CoreSchema:
43
+ # Make UnrecognizedStr only work in lax mode, not strict mode
44
+ # This makes it a "fallback" option when more specific types (like Literals) don't match
45
+ def validate_lax(v: Any) -> 'UnrecognizedStr':
46
+ if isinstance(v, cls):
47
+ return v
48
+ return cls(str(v))
49
+
50
+ # Use lax_or_strict_schema where strict always fails
51
+ # This forces Pydantic to prefer other union members in strict mode
52
+ # and only fall back to UnrecognizedStr in lax mode
53
+ return core_schema.lax_or_strict_schema(
54
+ lax_schema=core_schema.chain_schema([
55
+ core_schema.str_schema(),
56
+ core_schema.no_info_plain_validator_function(validate_lax)
57
+ ]),
58
+ strict_schema=core_schema.none_schema(), # Always fails in strict mode
59
+ )
60
+
61
+
62
+ class UnrecognizedInt(int):
63
+ @classmethod
64
+ def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> core_schema.CoreSchema:
65
+ # Make UnrecognizedInt only work in lax mode, not strict mode
66
+ # This makes it a "fallback" option when more specific types (like Literals) don't match
67
+ def validate_lax(v: Any) -> 'UnrecognizedInt':
68
+ if isinstance(v, cls):
69
+ return v
70
+ return cls(int(v))
71
+ return core_schema.lax_or_strict_schema(
72
+ lax_schema=core_schema.chain_schema([
73
+ core_schema.int_schema(),
74
+ core_schema.no_info_plain_validator_function(validate_lax)
75
+ ]),
76
+ strict_schema=core_schema.none_schema(), # Always fails in strict mode
77
+ )
gr4vy/utils/__init__.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from typing import TYPE_CHECKING
4
4
  from importlib import import_module
5
5
  import builtins
6
+ import sys
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from .annotations import get_discriminator
@@ -41,7 +42,6 @@ if TYPE_CHECKING:
41
42
  validate_decimal,
42
43
  validate_float,
43
44
  validate_int,
44
- validate_open_enum,
45
45
  )
46
46
  from .url import generate_url, template_url, remove_suffix
47
47
  from .values import (
@@ -103,7 +103,6 @@ __all__ = [
103
103
  "validate_const",
104
104
  "validate_float",
105
105
  "validate_int",
106
- "validate_open_enum",
107
106
  "cast_partial",
108
107
  ]
109
108
 
@@ -157,11 +156,22 @@ _dynamic_imports: dict[str, str] = {
157
156
  "validate_const": ".serializers",
158
157
  "validate_float": ".serializers",
159
158
  "validate_int": ".serializers",
160
- "validate_open_enum": ".serializers",
161
159
  "cast_partial": ".values",
162
160
  }
163
161
 
164
162
 
163
+ def dynamic_import(modname, retries=3):
164
+ for attempt in range(retries):
165
+ try:
166
+ return import_module(modname, __package__)
167
+ except KeyError:
168
+ # Clear any half-initialized module and retry
169
+ sys.modules.pop(modname, None)
170
+ if attempt == retries - 1:
171
+ break
172
+ raise KeyError(f"Failed to import module '{modname}' after {retries} attempts")
173
+
174
+
165
175
  def __getattr__(attr_name: str) -> object:
166
176
  module_name = _dynamic_imports.get(attr_name)
167
177
  if module_name is None:
@@ -170,9 +180,8 @@ def __getattr__(attr_name: str) -> object:
170
180
  )
171
181
 
172
182
  try:
173
- module = import_module(module_name, __package__)
174
- result = getattr(module, attr_name)
175
- return result
183
+ module = dynamic_import(module_name)
184
+ return getattr(module, attr_name)
176
185
  except ImportError as e:
177
186
  raise ImportError(
178
187
  f"Failed to import {attr_name} from {module_name}: {e}"
@@ -3,6 +3,7 @@
3
3
  from enum import Enum
4
4
  from typing import Any, Optional
5
5
 
6
+
6
7
  def get_discriminator(model: Any, fieldname: str, key: str) -> str:
7
8
  """
8
9
  Recursively search for the discriminator attribute in a model.
@@ -25,31 +26,54 @@ def get_discriminator(model: Any, fieldname: str, key: str) -> str:
25
26
 
26
27
  if isinstance(field, dict):
27
28
  if key in field:
28
- return f'{field[key]}'
29
+ return f"{field[key]}"
29
30
 
30
31
  if hasattr(field, fieldname):
31
32
  attr = getattr(field, fieldname)
32
33
  if isinstance(attr, Enum):
33
- return f'{attr.value}'
34
- return f'{attr}'
34
+ return f"{attr.value}"
35
+ return f"{attr}"
35
36
 
36
37
  if hasattr(field, upper_fieldname):
37
38
  attr = getattr(field, upper_fieldname)
38
39
  if isinstance(attr, Enum):
39
- return f'{attr.value}'
40
- return f'{attr}'
40
+ return f"{attr.value}"
41
+ return f"{attr}"
41
42
 
42
43
  return None
43
44
 
45
+ def search_nested_discriminator(obj: Any) -> Optional[str]:
46
+ """Recursively search for discriminator in nested structures."""
47
+ # First try direct field lookup
48
+ discriminator = get_field_discriminator(obj)
49
+ if discriminator is not None:
50
+ return discriminator
51
+
52
+ # If it's a dict, search in nested values
53
+ if isinstance(obj, dict):
54
+ for value in obj.values():
55
+ if isinstance(value, list):
56
+ # Search in list items
57
+ for item in value:
58
+ nested_discriminator = search_nested_discriminator(item)
59
+ if nested_discriminator is not None:
60
+ return nested_discriminator
61
+ elif isinstance(value, dict):
62
+ # Search in nested dict
63
+ nested_discriminator = search_nested_discriminator(value)
64
+ if nested_discriminator is not None:
65
+ return nested_discriminator
66
+
67
+ return None
44
68
 
45
69
  if isinstance(model, list):
46
70
  for field in model:
47
- discriminator = get_field_discriminator(field)
71
+ discriminator = search_nested_discriminator(field)
48
72
  if discriminator is not None:
49
73
  return discriminator
50
74
 
51
- discriminator = get_field_discriminator(model)
75
+ discriminator = search_nested_discriminator(model)
52
76
  if discriminator is not None:
53
77
  return discriminator
54
78
 
55
- raise ValueError(f'Could not find discriminator field {fieldname} in {model}')
79
+ raise ValueError(f"Could not find discriminator field {fieldname} in {model}")
gr4vy/utils/enums.py CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  import enum
4
4
  import sys
5
+ from typing import Any
6
+
7
+ from pydantic_core import core_schema
8
+
5
9
 
6
10
  class OpenEnumMeta(enum.EnumMeta):
7
11
  # The __call__ method `boundary` kwarg was added in 3.11 and must be present
@@ -72,3 +76,59 @@ class OpenEnumMeta(enum.EnumMeta):
72
76
  )
73
77
  except ValueError:
74
78
  return value
79
+
80
+ def __new__(mcs, name, bases, namespace, **kwargs):
81
+ cls = super().__new__(mcs, name, bases, namespace, **kwargs)
82
+
83
+ # Add __get_pydantic_core_schema__ to make open enums work correctly
84
+ # in union discrimination. In strict mode (used by Pydantic for unions),
85
+ # only known enum values match. In lax mode, unknown values are accepted.
86
+ def __get_pydantic_core_schema__(
87
+ cls_inner: Any, _source_type: Any, _handler: Any
88
+ ) -> core_schema.CoreSchema:
89
+ # Create a validator that only accepts known enum values (for strict mode)
90
+ def validate_strict(v: Any) -> Any:
91
+ if isinstance(v, cls_inner):
92
+ return v
93
+ # Use the parent EnumMeta's __call__ which raises ValueError for unknown values
94
+ return enum.EnumMeta.__call__(cls_inner, v)
95
+
96
+ # Create a lax validator that accepts unknown values
97
+ def validate_lax(v: Any) -> Any:
98
+ if isinstance(v, cls_inner):
99
+ return v
100
+ try:
101
+ return enum.EnumMeta.__call__(cls_inner, v)
102
+ except ValueError:
103
+ # Return the raw value for unknown enum values
104
+ return v
105
+
106
+ # Determine the base type schema (str or int)
107
+ is_int_enum = False
108
+ for base in cls_inner.__mro__:
109
+ if base is int:
110
+ is_int_enum = True
111
+ break
112
+ if base is str:
113
+ break
114
+
115
+ base_schema = (
116
+ core_schema.int_schema()
117
+ if is_int_enum
118
+ else core_schema.str_schema()
119
+ )
120
+
121
+ # Use lax_or_strict_schema:
122
+ # - strict mode: only known enum values match (raises ValueError for unknown)
123
+ # - lax mode: accept any value, return enum member or raw value
124
+ return core_schema.lax_or_strict_schema(
125
+ lax_schema=core_schema.chain_schema(
126
+ [base_schema, core_schema.no_info_plain_validator_function(validate_lax)]
127
+ ),
128
+ strict_schema=core_schema.chain_schema(
129
+ [base_schema, core_schema.no_info_plain_validator_function(validate_strict)]
130
+ ),
131
+ )
132
+
133
+ setattr(cls, "__get_pydantic_core_schema__", classmethod(__get_pydantic_core_schema__))
134
+ return cls
@@ -17,6 +17,9 @@ T = TypeVar("T")
17
17
 
18
18
 
19
19
  class EventStream(Generic[T]):
20
+ # Holds a reference to the SDK client to avoid it being garbage collected
21
+ # and cause termination of the underlying httpx client.
22
+ client_ref: Optional[object]
20
23
  response: httpx.Response
21
24
  generator: Generator[T, None, None]
22
25
 
@@ -25,9 +28,11 @@ class EventStream(Generic[T]):
25
28
  response: httpx.Response,
26
29
  decoder: Callable[[str], T],
27
30
  sentinel: Optional[str] = None,
31
+ client_ref: Optional[object] = None,
28
32
  ):
29
33
  self.response = response
30
34
  self.generator = stream_events(response, decoder, sentinel)
35
+ self.client_ref = client_ref
31
36
 
32
37
  def __iter__(self):
33
38
  return self
@@ -43,6 +48,9 @@ class EventStream(Generic[T]):
43
48
 
44
49
 
45
50
  class EventStreamAsync(Generic[T]):
51
+ # Holds a reference to the SDK client to avoid it being garbage collected
52
+ # and cause termination of the underlying httpx client.
53
+ client_ref: Optional[object]
46
54
  response: httpx.Response
47
55
  generator: AsyncGenerator[T, None]
48
56
 
@@ -51,9 +59,11 @@ class EventStreamAsync(Generic[T]):
51
59
  response: httpx.Response,
52
60
  decoder: Callable[[str], T],
53
61
  sentinel: Optional[str] = None,
62
+ client_ref: Optional[object] = None,
54
63
  ):
55
64
  self.response = response
56
65
  self.generator = stream_events_async(response, decoder, sentinel)
66
+ self.client_ref = client_ref
57
67
 
58
68
  def __aiter__(self):
59
69
  return self
gr4vy/utils/forms.py CHANGED
@@ -142,16 +142,21 @@ def serialize_multipart_form(
142
142
  if field_metadata.file:
143
143
  if isinstance(val, List):
144
144
  # Handle array of files
145
+ array_field_name = f_name + "[]"
145
146
  for file_obj in val:
146
147
  if not _is_set(file_obj):
147
148
  continue
148
-
149
- file_name, content, content_type = _extract_file_properties(file_obj)
149
+
150
+ file_name, content, content_type = _extract_file_properties(
151
+ file_obj
152
+ )
150
153
 
151
154
  if content_type is not None:
152
- files.append((f_name + "[]", (file_name, content, content_type)))
155
+ files.append(
156
+ (array_field_name, (file_name, content, content_type))
157
+ )
153
158
  else:
154
- files.append((f_name + "[]", (file_name, content)))
159
+ files.append((array_field_name, (file_name, content)))
155
160
  else:
156
161
  # Handle single file
157
162
  file_name, content, content_type = _extract_file_properties(val)
@@ -161,11 +166,16 @@ def serialize_multipart_form(
161
166
  else:
162
167
  files.append((f_name, (file_name, content)))
163
168
  elif field_metadata.json:
164
- files.append((f_name, (
165
- None,
166
- marshal_json(val, request_field_types[name]),
167
- "application/json",
168
- )))
169
+ files.append(
170
+ (
171
+ f_name,
172
+ (
173
+ None,
174
+ marshal_json(val, request_field_types[name]),
175
+ "application/json",
176
+ ),
177
+ )
178
+ )
169
179
  else:
170
180
  if isinstance(val, List):
171
181
  values = []
@@ -175,7 +185,8 @@ def serialize_multipart_form(
175
185
  continue
176
186
  values.append(_val_to_string(value))
177
187
 
178
- form[f_name + "[]"] = values
188
+ array_field_name = f_name + "[]"
189
+ form[array_field_name] = values
179
190
  else:
180
191
  form[f_name] = _val_to_string(val)
181
192
  return media_type, form, files
@@ -27,12 +27,13 @@ from .forms import _populate_form
27
27
  def get_query_params(
28
28
  query_params: Any,
29
29
  gbls: Optional[Any] = None,
30
+ allow_empty_value: Optional[List[str]] = None,
30
31
  ) -> Dict[str, List[str]]:
31
32
  params: Dict[str, List[str]] = {}
32
33
 
33
- globals_already_populated = _populate_query_params(query_params, gbls, params, [])
34
+ globals_already_populated = _populate_query_params(query_params, gbls, params, [], allow_empty_value)
34
35
  if _is_set(gbls):
35
- _populate_query_params(gbls, None, params, globals_already_populated)
36
+ _populate_query_params(gbls, None, params, globals_already_populated, allow_empty_value)
36
37
 
37
38
  return params
38
39
 
@@ -42,6 +43,7 @@ def _populate_query_params(
42
43
  gbls: Any,
43
44
  query_param_values: Dict[str, List[str]],
44
45
  skip_fields: List[str],
46
+ allow_empty_value: Optional[List[str]] = None,
45
47
  ) -> List[str]:
46
48
  globals_already_populated: List[str] = []
47
49
 
@@ -69,6 +71,16 @@ def _populate_query_params(
69
71
  globals_already_populated.append(name)
70
72
 
71
73
  f_name = field.alias if field.alias is not None else name
74
+
75
+ allow_empty_set = set(allow_empty_value or [])
76
+ should_include_empty = f_name in allow_empty_set and (
77
+ value is None or value == [] or value == ""
78
+ )
79
+
80
+ if should_include_empty:
81
+ query_param_values[f_name] = [""]
82
+ continue
83
+
72
84
  serialization = metadata.serialization
73
85
  if serialization is not None:
74
86
  serialized_parms = _get_serialized_params(
@@ -44,7 +44,7 @@ def serialize_request_body(
44
44
 
45
45
  serialized_request_body = SerializedRequestBody(media_type)
46
46
 
47
- if re.match(r"(application|text)\/.*?\+*json.*", media_type) is not None:
47
+ if re.match(r"(application|text)\/([^+]+\+)*json.*", media_type) is not None:
48
48
  serialized_request_body.content = marshal_json(request_body, request_body_type)
49
49
  elif re.match(r"multipart\/.*", media_type) is not None:
50
50
  (