dub 0.33.0__py3-none-any.whl → 0.34.1__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 (65) hide show
  1. dub/_version.py +3 -3
  2. dub/analytics.py +2 -0
  3. dub/basesdk.py +6 -0
  4. dub/bounties.py +841 -0
  5. dub/commissions.py +4 -0
  6. dub/customers.py +8 -0
  7. dub/domains.py +12 -0
  8. dub/embed_tokens.py +2 -0
  9. dub/events.py +2 -0
  10. dub/folders.py +8 -0
  11. dub/links.py +20 -0
  12. dub/models/components/__init__.py +55 -149
  13. dub/models/components/commissioncreatedevent.py +29 -1
  14. dub/models/components/leadcreatedevent.py +8 -8
  15. dub/models/components/linkclickedevent.py +12 -12
  16. dub/models/components/linkerrorschema.py +12 -12
  17. dub/models/components/linkschema.py +3 -3
  18. dub/models/components/linktagschema.py +3 -3
  19. dub/models/components/linktagschemaoutput.py +38 -0
  20. dub/models/components/linkwebhookevent.py +8 -10
  21. dub/models/components/partnerenrolledevent.py +4 -4
  22. dub/models/components/salecreatedevent.py +8 -8
  23. dub/models/operations/__init__.py +223 -22
  24. dub/models/operations/approvebountysubmission.py +185 -0
  25. dub/models/operations/createpartner.py +4 -55
  26. dub/models/operations/createpartnerlink.py +0 -51
  27. dub/models/operations/createreferralsembedtoken.py +0 -51
  28. dub/models/operations/getcustomers.py +18 -0
  29. dub/models/operations/getlinkinfo.py +0 -2
  30. dub/models/operations/getlinks.py +2 -2
  31. dub/models/operations/getlinkscount.py +2 -2
  32. dub/models/operations/getqrcode.py +1 -1
  33. dub/models/operations/listbountysubmissions.py +212 -0
  34. dub/models/operations/listdomains.py +1 -1
  35. dub/models/operations/listevents.py +2016 -21
  36. dub/models/operations/listpartners.py +4 -4
  37. dub/models/operations/rejectbountysubmission.py +174 -0
  38. dub/models/operations/retrieveanalytics.py +16 -5
  39. dub/models/operations/retrievelinks.py +2 -2
  40. dub/models/operations/tracklead.py +4 -4
  41. dub/models/operations/updatecustomer.py +23 -11
  42. dub/models/operations/updatelink.py +0 -2
  43. dub/models/operations/updateworkspace.py +3 -3
  44. dub/models/operations/upsertpartnerlink.py +7 -65
  45. dub/partners.py +22 -4
  46. dub/qr_codes.py +2 -0
  47. dub/sdk.py +3 -0
  48. dub/tags.py +24 -12
  49. dub/track.py +4 -0
  50. dub/types/basemodel.py +41 -3
  51. dub/utils/__init__.py +0 -3
  52. dub/utils/enums.py +60 -0
  53. dub/utils/forms.py +21 -10
  54. dub/utils/queryparams.py +14 -2
  55. dub/utils/requestbodies.py +3 -3
  56. dub/utils/serializers.py +0 -20
  57. dub/workspaces.py +4 -0
  58. {dub-0.33.0.dist-info → dub-0.34.1.dist-info}/METADATA +20 -14
  59. {dub-0.33.0.dist-info → dub-0.34.1.dist-info}/RECORD +61 -60
  60. dub/models/components/clickevent.py +0 -557
  61. dub/models/components/continentcode.py +0 -16
  62. dub/models/components/leadevent.py +0 -681
  63. dub/models/components/saleevent.py +0 -780
  64. {dub-0.33.0.dist-info → dub-0.34.1.dist-info}/WHEEL +0 -0
  65. {dub-0.33.0.dist-info → dub-0.34.1.dist-info}/licenses/LICENSE +0 -0
dub/partners.py CHANGED
@@ -70,6 +70,7 @@ class Partners(BaseSDK):
70
70
  "json",
71
71
  Optional[operations.CreatePartnerRequestBody],
72
72
  ),
73
+ allow_empty_value=None,
73
74
  timeout_ms=timeout_ms,
74
75
  )
75
76
 
@@ -213,6 +214,7 @@ class Partners(BaseSDK):
213
214
  "json",
214
215
  Optional[operations.CreatePartnerRequestBody],
215
216
  ),
217
+ allow_empty_value=None,
216
218
  timeout_ms=timeout_ms,
217
219
  )
218
220
 
@@ -344,6 +346,7 @@ class Partners(BaseSDK):
344
346
  accept_header_value="application/json",
345
347
  http_headers=http_headers,
346
348
  security=self.sdk_configuration.security,
349
+ allow_empty_value=None,
347
350
  timeout_ms=timeout_ms,
348
351
  )
349
352
 
@@ -475,6 +478,7 @@ class Partners(BaseSDK):
475
478
  accept_header_value="application/json",
476
479
  http_headers=http_headers,
477
480
  security=self.sdk_configuration.security,
481
+ allow_empty_value=None,
478
482
  timeout_ms=timeout_ms,
479
483
  )
480
484
 
@@ -618,6 +622,7 @@ class Partners(BaseSDK):
618
622
  "json",
619
623
  Optional[operations.CreatePartnerLinkRequestBody],
620
624
  ),
625
+ allow_empty_value=None,
621
626
  timeout_ms=timeout_ms,
622
627
  )
623
628
 
@@ -759,6 +764,7 @@ class Partners(BaseSDK):
759
764
  "json",
760
765
  Optional[operations.CreatePartnerLinkRequestBody],
761
766
  ),
767
+ allow_empty_value=None,
762
768
  timeout_ms=timeout_ms,
763
769
  )
764
770
 
@@ -850,7 +856,7 @@ class Partners(BaseSDK):
850
856
  server_url: Optional[str] = None,
851
857
  timeout_ms: Optional[int] = None,
852
858
  http_headers: Optional[Mapping[str, str]] = None,
853
- ) -> List[operations.Link]:
859
+ ) -> List[operations.RetrieveLinksResponseBody]:
854
860
  r"""Retrieve a partner's links.
855
861
 
856
862
  Retrieve a partner's links by their partner ID or tenant ID.
@@ -888,6 +894,7 @@ class Partners(BaseSDK):
888
894
  accept_header_value="application/json",
889
895
  http_headers=http_headers,
890
896
  security=self.sdk_configuration.security,
897
+ allow_empty_value=None,
891
898
  timeout_ms=timeout_ms,
892
899
  )
893
900
 
@@ -926,7 +933,9 @@ class Partners(BaseSDK):
926
933
 
927
934
  response_data: Any = None
928
935
  if utils.match_response(http_res, "200", "application/json"):
929
- return unmarshal_json_response(List[operations.Link], http_res)
936
+ return unmarshal_json_response(
937
+ List[operations.RetrieveLinksResponseBody], http_res
938
+ )
930
939
  if utils.match_response(http_res, "400", "application/json"):
931
940
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
932
941
  raise errors.BadRequest(response_data, http_res)
@@ -979,7 +988,7 @@ class Partners(BaseSDK):
979
988
  server_url: Optional[str] = None,
980
989
  timeout_ms: Optional[int] = None,
981
990
  http_headers: Optional[Mapping[str, str]] = None,
982
- ) -> List[operations.Link]:
991
+ ) -> List[operations.RetrieveLinksResponseBody]:
983
992
  r"""Retrieve a partner's links.
984
993
 
985
994
  Retrieve a partner's links by their partner ID or tenant ID.
@@ -1017,6 +1026,7 @@ class Partners(BaseSDK):
1017
1026
  accept_header_value="application/json",
1018
1027
  http_headers=http_headers,
1019
1028
  security=self.sdk_configuration.security,
1029
+ allow_empty_value=None,
1020
1030
  timeout_ms=timeout_ms,
1021
1031
  )
1022
1032
 
@@ -1055,7 +1065,9 @@ class Partners(BaseSDK):
1055
1065
 
1056
1066
  response_data: Any = None
1057
1067
  if utils.match_response(http_res, "200", "application/json"):
1058
- return unmarshal_json_response(List[operations.Link], http_res)
1068
+ return unmarshal_json_response(
1069
+ List[operations.RetrieveLinksResponseBody], http_res
1070
+ )
1059
1071
  if utils.match_response(http_res, "400", "application/json"):
1060
1072
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
1061
1073
  raise errors.BadRequest(response_data, http_res)
@@ -1158,6 +1170,7 @@ class Partners(BaseSDK):
1158
1170
  "json",
1159
1171
  Optional[operations.UpsertPartnerLinkRequestBody],
1160
1172
  ),
1173
+ allow_empty_value=None,
1161
1174
  timeout_ms=timeout_ms,
1162
1175
  )
1163
1176
 
@@ -1299,6 +1312,7 @@ class Partners(BaseSDK):
1299
1312
  "json",
1300
1313
  Optional[operations.UpsertPartnerLinkRequestBody],
1301
1314
  ),
1315
+ allow_empty_value=None,
1302
1316
  timeout_ms=timeout_ms,
1303
1317
  )
1304
1318
 
@@ -1431,6 +1445,7 @@ class Partners(BaseSDK):
1431
1445
  accept_header_value="application/json",
1432
1446
  http_headers=http_headers,
1433
1447
  security=self.sdk_configuration.security,
1448
+ allow_empty_value=None,
1434
1449
  timeout_ms=timeout_ms,
1435
1450
  )
1436
1451
 
@@ -1565,6 +1580,7 @@ class Partners(BaseSDK):
1565
1580
  accept_header_value="application/json",
1566
1581
  http_headers=http_headers,
1567
1582
  security=self.sdk_configuration.security,
1583
+ allow_empty_value=None,
1568
1584
  timeout_ms=timeout_ms,
1569
1585
  )
1570
1586
 
@@ -1704,6 +1720,7 @@ class Partners(BaseSDK):
1704
1720
  get_serialized_body=lambda: utils.serialize_request_body(
1705
1721
  request, False, True, "json", Optional[operations.BanPartnerRequestBody]
1706
1722
  ),
1723
+ allow_empty_value=None,
1707
1724
  timeout_ms=timeout_ms,
1708
1725
  )
1709
1726
 
@@ -1841,6 +1858,7 @@ class Partners(BaseSDK):
1841
1858
  get_serialized_body=lambda: utils.serialize_request_body(
1842
1859
  request, False, True, "json", Optional[operations.BanPartnerRequestBody]
1843
1860
  ),
1861
+ allow_empty_value=None,
1844
1862
  timeout_ms=timeout_ms,
1845
1863
  )
1846
1864
 
dub/qr_codes.py CHANGED
@@ -58,6 +58,7 @@ class QRCodes(BaseSDK):
58
58
  accept_header_value="image/png",
59
59
  http_headers=http_headers,
60
60
  security=self.sdk_configuration.security,
61
+ allow_empty_value=None,
61
62
  timeout_ms=timeout_ms,
62
63
  )
63
64
 
@@ -187,6 +188,7 @@ class QRCodes(BaseSDK):
187
188
  accept_header_value="image/png",
188
189
  http_headers=http_headers,
189
190
  security=self.sdk_configuration.security,
191
+ allow_empty_value=None,
190
192
  timeout_ms=timeout_ms,
191
193
  )
192
194
 
dub/sdk.py CHANGED
@@ -17,6 +17,7 @@ import weakref
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from dub.analytics import Analytics
20
+ from dub.bounties import Bounties
20
21
  from dub.commissions import Commissions
21
22
  from dub.customers import Customers
22
23
  from dub.domains import Domains
@@ -47,6 +48,7 @@ class Dub(BaseSDK):
47
48
  workspaces: "Workspaces"
48
49
  embed_tokens: "EmbedTokens"
49
50
  qr_codes: "QRCodes"
51
+ bounties: "Bounties"
50
52
  _sub_sdk_map = {
51
53
  "links": ("dub.links", "Links"),
52
54
  "analytics": ("dub.analytics", "Analytics"),
@@ -61,6 +63,7 @@ class Dub(BaseSDK):
61
63
  "workspaces": ("dub.workspaces", "Workspaces"),
62
64
  "embed_tokens": ("dub.embed_tokens", "EmbedTokens"),
63
65
  "qr_codes": ("dub.qr_codes", "QRCodes"),
66
+ "bounties": ("dub.bounties", "Bounties"),
64
67
  }
65
68
 
66
69
  def __init__(
dub/tags.py CHANGED
@@ -23,7 +23,7 @@ class Tags(BaseSDK):
23
23
  server_url: Optional[str] = None,
24
24
  timeout_ms: Optional[int] = None,
25
25
  http_headers: Optional[Mapping[str, str]] = None,
26
- ) -> components.LinkTagSchema:
26
+ ) -> components.LinkTagSchemaOutput:
27
27
  r"""Create a tag
28
28
 
29
29
  Create a tag for the authenticated workspace.
@@ -66,6 +66,7 @@ class Tags(BaseSDK):
66
66
  get_serialized_body=lambda: utils.serialize_request_body(
67
67
  request, False, True, "json", Optional[operations.CreateTagRequestBody]
68
68
  ),
69
+ allow_empty_value=None,
69
70
  timeout_ms=timeout_ms,
70
71
  )
71
72
 
@@ -104,7 +105,7 @@ class Tags(BaseSDK):
104
105
 
105
106
  response_data: Any = None
106
107
  if utils.match_response(http_res, "201", "application/json"):
107
- return unmarshal_json_response(components.LinkTagSchema, http_res)
108
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
108
109
  if utils.match_response(http_res, "400", "application/json"):
109
110
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
110
111
  raise errors.BadRequest(response_data, http_res)
@@ -160,7 +161,7 @@ class Tags(BaseSDK):
160
161
  server_url: Optional[str] = None,
161
162
  timeout_ms: Optional[int] = None,
162
163
  http_headers: Optional[Mapping[str, str]] = None,
163
- ) -> components.LinkTagSchema:
164
+ ) -> components.LinkTagSchemaOutput:
164
165
  r"""Create a tag
165
166
 
166
167
  Create a tag for the authenticated workspace.
@@ -203,6 +204,7 @@ class Tags(BaseSDK):
203
204
  get_serialized_body=lambda: utils.serialize_request_body(
204
205
  request, False, True, "json", Optional[operations.CreateTagRequestBody]
205
206
  ),
207
+ allow_empty_value=None,
206
208
  timeout_ms=timeout_ms,
207
209
  )
208
210
 
@@ -241,7 +243,7 @@ class Tags(BaseSDK):
241
243
 
242
244
  response_data: Any = None
243
245
  if utils.match_response(http_res, "201", "application/json"):
244
- return unmarshal_json_response(components.LinkTagSchema, http_res)
246
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
245
247
  if utils.match_response(http_res, "400", "application/json"):
246
248
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
247
249
  raise errors.BadRequest(response_data, http_res)
@@ -292,7 +294,7 @@ class Tags(BaseSDK):
292
294
  server_url: Optional[str] = None,
293
295
  timeout_ms: Optional[int] = None,
294
296
  http_headers: Optional[Mapping[str, str]] = None,
295
- ) -> List[components.LinkTagSchema]:
297
+ ) -> List[components.LinkTagSchemaOutput]:
296
298
  r"""Retrieve a list of tags
297
299
 
298
300
  Retrieve a list of tags for the authenticated workspace.
@@ -330,6 +332,7 @@ class Tags(BaseSDK):
330
332
  accept_header_value="application/json",
331
333
  http_headers=http_headers,
332
334
  security=self.sdk_configuration.security,
335
+ allow_empty_value=None,
333
336
  timeout_ms=timeout_ms,
334
337
  )
335
338
 
@@ -368,7 +371,9 @@ class Tags(BaseSDK):
368
371
 
369
372
  response_data: Any = None
370
373
  if utils.match_response(http_res, "200", "application/json"):
371
- return unmarshal_json_response(List[components.LinkTagSchema], http_res)
374
+ return unmarshal_json_response(
375
+ List[components.LinkTagSchemaOutput], http_res
376
+ )
372
377
  if utils.match_response(http_res, "400", "application/json"):
373
378
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
374
379
  raise errors.BadRequest(response_data, http_res)
@@ -419,7 +424,7 @@ class Tags(BaseSDK):
419
424
  server_url: Optional[str] = None,
420
425
  timeout_ms: Optional[int] = None,
421
426
  http_headers: Optional[Mapping[str, str]] = None,
422
- ) -> List[components.LinkTagSchema]:
427
+ ) -> List[components.LinkTagSchemaOutput]:
423
428
  r"""Retrieve a list of tags
424
429
 
425
430
  Retrieve a list of tags for the authenticated workspace.
@@ -457,6 +462,7 @@ class Tags(BaseSDK):
457
462
  accept_header_value="application/json",
458
463
  http_headers=http_headers,
459
464
  security=self.sdk_configuration.security,
465
+ allow_empty_value=None,
460
466
  timeout_ms=timeout_ms,
461
467
  )
462
468
 
@@ -495,7 +501,9 @@ class Tags(BaseSDK):
495
501
 
496
502
  response_data: Any = None
497
503
  if utils.match_response(http_res, "200", "application/json"):
498
- return unmarshal_json_response(List[components.LinkTagSchema], http_res)
504
+ return unmarshal_json_response(
505
+ List[components.LinkTagSchemaOutput], http_res
506
+ )
499
507
  if utils.match_response(http_res, "400", "application/json"):
500
508
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
501
509
  raise errors.BadRequest(response_data, http_res)
@@ -552,7 +560,7 @@ class Tags(BaseSDK):
552
560
  server_url: Optional[str] = None,
553
561
  timeout_ms: Optional[int] = None,
554
562
  http_headers: Optional[Mapping[str, str]] = None,
555
- ) -> components.LinkTagSchema:
563
+ ) -> components.LinkTagSchemaOutput:
556
564
  r"""Update a tag
557
565
 
558
566
  Update a tag in the workspace.
@@ -601,6 +609,7 @@ class Tags(BaseSDK):
601
609
  "json",
602
610
  Optional[operations.UpdateTagRequestBody],
603
611
  ),
612
+ allow_empty_value=None,
604
613
  timeout_ms=timeout_ms,
605
614
  )
606
615
 
@@ -639,7 +648,7 @@ class Tags(BaseSDK):
639
648
 
640
649
  response_data: Any = None
641
650
  if utils.match_response(http_res, "200", "application/json"):
642
- return unmarshal_json_response(components.LinkTagSchema, http_res)
651
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
643
652
  if utils.match_response(http_res, "400", "application/json"):
644
653
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
645
654
  raise errors.BadRequest(response_data, http_res)
@@ -696,7 +705,7 @@ class Tags(BaseSDK):
696
705
  server_url: Optional[str] = None,
697
706
  timeout_ms: Optional[int] = None,
698
707
  http_headers: Optional[Mapping[str, str]] = None,
699
- ) -> components.LinkTagSchema:
708
+ ) -> components.LinkTagSchemaOutput:
700
709
  r"""Update a tag
701
710
 
702
711
  Update a tag in the workspace.
@@ -745,6 +754,7 @@ class Tags(BaseSDK):
745
754
  "json",
746
755
  Optional[operations.UpdateTagRequestBody],
747
756
  ),
757
+ allow_empty_value=None,
748
758
  timeout_ms=timeout_ms,
749
759
  )
750
760
 
@@ -783,7 +793,7 @@ class Tags(BaseSDK):
783
793
 
784
794
  response_data: Any = None
785
795
  if utils.match_response(http_res, "200", "application/json"):
786
- return unmarshal_json_response(components.LinkTagSchema, http_res)
796
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
787
797
  if utils.match_response(http_res, "400", "application/json"):
788
798
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
789
799
  raise errors.BadRequest(response_data, http_res)
@@ -872,6 +882,7 @@ class Tags(BaseSDK):
872
882
  accept_header_value="application/json",
873
883
  http_headers=http_headers,
874
884
  security=self.sdk_configuration.security,
885
+ allow_empty_value=None,
875
886
  timeout_ms=timeout_ms,
876
887
  )
877
888
 
@@ -999,6 +1010,7 @@ class Tags(BaseSDK):
999
1010
  accept_header_value="application/json",
1000
1011
  http_headers=http_headers,
1001
1012
  security=self.sdk_configuration.security,
1013
+ allow_empty_value=None,
1002
1014
  timeout_ms=timeout_ms,
1003
1015
  )
1004
1016
 
dub/track.py CHANGED
@@ -66,6 +66,7 @@ class Track(BaseSDK):
66
66
  get_serialized_body=lambda: utils.serialize_request_body(
67
67
  request, False, True, "json", Optional[operations.TrackLeadRequestBody]
68
68
  ),
69
+ allow_empty_value=None,
69
70
  timeout_ms=timeout_ms,
70
71
  )
71
72
 
@@ -203,6 +204,7 @@ class Track(BaseSDK):
203
204
  get_serialized_body=lambda: utils.serialize_request_body(
204
205
  request, False, True, "json", Optional[operations.TrackLeadRequestBody]
205
206
  ),
207
+ allow_empty_value=None,
206
208
  timeout_ms=timeout_ms,
207
209
  )
208
210
 
@@ -340,6 +342,7 @@ class Track(BaseSDK):
340
342
  get_serialized_body=lambda: utils.serialize_request_body(
341
343
  request, False, True, "json", Optional[operations.TrackSaleRequestBody]
342
344
  ),
345
+ allow_empty_value=None,
343
346
  timeout_ms=timeout_ms,
344
347
  )
345
348
 
@@ -477,6 +480,7 @@ class Track(BaseSDK):
477
480
  get_serialized_body=lambda: utils.serialize_request_body(
478
481
  request, False, True, "json", Optional[operations.TrackSaleRequestBody]
479
482
  ),
483
+ allow_empty_value=None,
480
484
  timeout_ms=timeout_ms,
481
485
  )
482
486
 
dub/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
+ )
dub/utils/__init__.py CHANGED
@@ -41,7 +41,6 @@ if TYPE_CHECKING:
41
41
  validate_decimal,
42
42
  validate_float,
43
43
  validate_int,
44
- validate_open_enum,
45
44
  )
46
45
  from .url import generate_url, template_url, remove_suffix
47
46
  from .values import (
@@ -102,7 +101,6 @@ __all__ = [
102
101
  "validate_const",
103
102
  "validate_float",
104
103
  "validate_int",
105
- "validate_open_enum",
106
104
  "cast_partial",
107
105
  ]
108
106
 
@@ -155,7 +153,6 @@ _dynamic_imports: dict[str, str] = {
155
153
  "validate_const": ".serializers",
156
154
  "validate_float": ".serializers",
157
155
  "validate_int": ".serializers",
158
- "validate_open_enum": ".serializers",
159
156
  "cast_partial": ".values",
160
157
  }
161
158
 
dub/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
dub/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
dub/utils/queryparams.py CHANGED
@@ -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,15 +44,15 @@ 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
- elif re.match(r"multipart\/.*", media_type) is not None:
49
+ elif re.match(r"^multipart\/.*", media_type) is not None:
50
50
  (
51
51
  serialized_request_body.media_type,
52
52
  serialized_request_body.data,
53
53
  serialized_request_body.files,
54
54
  ) = serialize_multipart_form(media_type, request_body)
55
- elif re.match(r"application\/x-www-form-urlencoded.*", media_type) is not None:
55
+ elif re.match(r"^application\/x-www-form-urlencoded.*", media_type) is not None:
56
56
  serialized_request_body.data = serialize_form_data(request_body)
57
57
  elif isinstance(request_body, (bytes, bytearray, io.BytesIO, io.BufferedReader)):
58
58
  serialized_request_body.content = request_body
dub/utils/serializers.py CHANGED
@@ -102,26 +102,6 @@ def validate_int(b):
102
102
  return int(b)
103
103
 
104
104
 
105
- def validate_open_enum(is_int: bool):
106
- def validate(e):
107
- if e is None:
108
- return None
109
-
110
- if isinstance(e, Unset):
111
- return e
112
-
113
- if is_int:
114
- if not isinstance(e, int):
115
- raise ValueError("Expected int")
116
- else:
117
- if not isinstance(e, str):
118
- raise ValueError("Expected string")
119
-
120
- return e
121
-
122
- return validate
123
-
124
-
125
105
  def validate_const(v):
126
106
  def validate(c):
127
107
  # Optional[T] is a Union[T, None]