dub 0.32.0__py3-none-any.whl → 0.34.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. dub/_version.py +3 -3
  2. dub/analytics.py +6 -4
  3. dub/basesdk.py +6 -0
  4. dub/commissions.py +12 -8
  5. dub/customers.py +24 -313
  6. dub/domains.py +34 -26
  7. dub/embed_tokens.py +6 -4
  8. dub/events.py +6 -4
  9. dub/folders.py +24 -20
  10. dub/links.py +58 -54
  11. dub/models/components/__init__.py +81 -149
  12. dub/models/components/analyticstopurls.py +2 -2
  13. dub/models/components/leadcreatedevent.py +15 -14
  14. dub/models/components/linkclickedevent.py +19 -18
  15. dub/models/components/linkerrorschema.py +12 -12
  16. dub/models/components/linkschema.py +9 -3
  17. dub/models/components/linktagschema.py +3 -3
  18. dub/models/components/linktagschemaoutput.py +38 -0
  19. dub/models/components/linkwebhookevent.py +15 -16
  20. dub/models/components/partnerapplicationsubmittedevent.py +269 -0
  21. dub/models/components/partnerenrolledevent.py +68 -8
  22. dub/models/components/salecreatedevent.py +15 -14
  23. dub/models/components/webhookevent.py +6 -0
  24. dub/models/components/workspaceschema.py +6 -0
  25. dub/models/operations/__init__.py +147 -57
  26. dub/models/operations/banpartner.py +83 -0
  27. dub/models/operations/createpartner.py +68 -59
  28. dub/models/operations/createpartnerlink.py +0 -51
  29. dub/models/operations/createreferralsembedtoken.py +0 -51
  30. dub/models/operations/getcustomers.py +18 -0
  31. dub/models/operations/getlinkinfo.py +0 -2
  32. dub/models/operations/getlinks.py +2 -2
  33. dub/models/operations/getlinkscount.py +2 -2
  34. dub/models/operations/getqrcode.py +1 -1
  35. dub/models/operations/listcommissions.py +13 -2
  36. dub/models/operations/listdomains.py +1 -1
  37. dub/models/operations/listevents.py +2026 -21
  38. dub/models/operations/listpartners.py +75 -8
  39. dub/models/operations/retrieveanalytics.py +28 -5
  40. dub/models/operations/retrievelinks.py +44 -9
  41. dub/models/operations/retrievepartneranalytics.py +51 -11
  42. dub/models/operations/tracklead.py +4 -4
  43. dub/models/operations/updatecommission.py +7 -2
  44. dub/models/operations/updatecustomer.py +23 -11
  45. dub/models/operations/updatelink.py +0 -2
  46. dub/models/operations/updateworkspace.py +3 -3
  47. dub/models/operations/upsertpartnerlink.py +0 -51
  48. dub/partners.py +316 -24
  49. dub/qr_codes.py +4 -2
  50. dub/tags.py +24 -20
  51. dub/track.py +12 -16
  52. dub/types/basemodel.py +41 -3
  53. dub/utils/__init__.py +0 -3
  54. dub/utils/enums.py +60 -0
  55. dub/utils/forms.py +21 -10
  56. dub/utils/queryparams.py +14 -2
  57. dub/utils/requestbodies.py +3 -3
  58. dub/utils/retries.py +69 -5
  59. dub/utils/serializers.py +0 -20
  60. dub/utils/unmarshal_json_response.py +15 -1
  61. dub/workspaces.py +12 -16
  62. {dub-0.32.0.dist-info → dub-0.34.0.dist-info}/METADATA +15 -33
  63. {dub-0.32.0.dist-info → dub-0.34.0.dist-info}/RECORD +65 -67
  64. dub/models/components/clickevent.py +0 -556
  65. dub/models/components/continentcode.py +0 -16
  66. dub/models/components/leadevent.py +0 -680
  67. dub/models/components/saleevent.py +0 -779
  68. dub/models/operations/createcustomer.py +0 -382
  69. {dub-0.32.0.dist-info → dub-0.34.0.dist-info}/WHEEL +0 -0
  70. {dub-0.32.0.dist-info → dub-0.34.0.dist-info}/licenses/LICENSE +0 -0
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
- ) -> Optional[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(Optional[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
- ) -> Optional[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(Optional[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
- ) -> Optional[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
 
@@ -369,7 +372,7 @@ class Tags(BaseSDK):
369
372
  response_data: Any = None
370
373
  if utils.match_response(http_res, "200", "application/json"):
371
374
  return unmarshal_json_response(
372
- Optional[List[components.LinkTagSchema]], http_res
375
+ List[components.LinkTagSchemaOutput], http_res
373
376
  )
374
377
  if utils.match_response(http_res, "400", "application/json"):
375
378
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
@@ -421,7 +424,7 @@ class Tags(BaseSDK):
421
424
  server_url: Optional[str] = None,
422
425
  timeout_ms: Optional[int] = None,
423
426
  http_headers: Optional[Mapping[str, str]] = None,
424
- ) -> Optional[List[components.LinkTagSchema]]:
427
+ ) -> List[components.LinkTagSchemaOutput]:
425
428
  r"""Retrieve a list of tags
426
429
 
427
430
  Retrieve a list of tags for the authenticated workspace.
@@ -459,6 +462,7 @@ class Tags(BaseSDK):
459
462
  accept_header_value="application/json",
460
463
  http_headers=http_headers,
461
464
  security=self.sdk_configuration.security,
465
+ allow_empty_value=None,
462
466
  timeout_ms=timeout_ms,
463
467
  )
464
468
 
@@ -498,7 +502,7 @@ class Tags(BaseSDK):
498
502
  response_data: Any = None
499
503
  if utils.match_response(http_res, "200", "application/json"):
500
504
  return unmarshal_json_response(
501
- Optional[List[components.LinkTagSchema]], http_res
505
+ List[components.LinkTagSchemaOutput], http_res
502
506
  )
503
507
  if utils.match_response(http_res, "400", "application/json"):
504
508
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
@@ -556,7 +560,7 @@ class Tags(BaseSDK):
556
560
  server_url: Optional[str] = None,
557
561
  timeout_ms: Optional[int] = None,
558
562
  http_headers: Optional[Mapping[str, str]] = None,
559
- ) -> Optional[components.LinkTagSchema]:
563
+ ) -> components.LinkTagSchemaOutput:
560
564
  r"""Update a tag
561
565
 
562
566
  Update a tag in the workspace.
@@ -605,6 +609,7 @@ class Tags(BaseSDK):
605
609
  "json",
606
610
  Optional[operations.UpdateTagRequestBody],
607
611
  ),
612
+ allow_empty_value=None,
608
613
  timeout_ms=timeout_ms,
609
614
  )
610
615
 
@@ -643,7 +648,7 @@ class Tags(BaseSDK):
643
648
 
644
649
  response_data: Any = None
645
650
  if utils.match_response(http_res, "200", "application/json"):
646
- return unmarshal_json_response(Optional[components.LinkTagSchema], http_res)
651
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
647
652
  if utils.match_response(http_res, "400", "application/json"):
648
653
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
649
654
  raise errors.BadRequest(response_data, http_res)
@@ -700,7 +705,7 @@ class Tags(BaseSDK):
700
705
  server_url: Optional[str] = None,
701
706
  timeout_ms: Optional[int] = None,
702
707
  http_headers: Optional[Mapping[str, str]] = None,
703
- ) -> Optional[components.LinkTagSchema]:
708
+ ) -> components.LinkTagSchemaOutput:
704
709
  r"""Update a tag
705
710
 
706
711
  Update a tag in the workspace.
@@ -749,6 +754,7 @@ class Tags(BaseSDK):
749
754
  "json",
750
755
  Optional[operations.UpdateTagRequestBody],
751
756
  ),
757
+ allow_empty_value=None,
752
758
  timeout_ms=timeout_ms,
753
759
  )
754
760
 
@@ -787,7 +793,7 @@ class Tags(BaseSDK):
787
793
 
788
794
  response_data: Any = None
789
795
  if utils.match_response(http_res, "200", "application/json"):
790
- return unmarshal_json_response(Optional[components.LinkTagSchema], http_res)
796
+ return unmarshal_json_response(components.LinkTagSchemaOutput, http_res)
791
797
  if utils.match_response(http_res, "400", "application/json"):
792
798
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
793
799
  raise errors.BadRequest(response_data, http_res)
@@ -838,7 +844,7 @@ class Tags(BaseSDK):
838
844
  server_url: Optional[str] = None,
839
845
  timeout_ms: Optional[int] = None,
840
846
  http_headers: Optional[Mapping[str, str]] = None,
841
- ) -> Optional[operations.DeleteTagResponseBody]:
847
+ ) -> operations.DeleteTagResponseBody:
842
848
  r"""Delete a tag
843
849
 
844
850
  Delete a tag from the workspace. All existing links will still work, but they will no longer be associated with this tag.
@@ -876,6 +882,7 @@ class Tags(BaseSDK):
876
882
  accept_header_value="application/json",
877
883
  http_headers=http_headers,
878
884
  security=self.sdk_configuration.security,
885
+ allow_empty_value=None,
879
886
  timeout_ms=timeout_ms,
880
887
  )
881
888
 
@@ -914,9 +921,7 @@ class Tags(BaseSDK):
914
921
 
915
922
  response_data: Any = None
916
923
  if utils.match_response(http_res, "200", "application/json"):
917
- return unmarshal_json_response(
918
- Optional[operations.DeleteTagResponseBody], http_res
919
- )
924
+ return unmarshal_json_response(operations.DeleteTagResponseBody, http_res)
920
925
  if utils.match_response(http_res, "400", "application/json"):
921
926
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
922
927
  raise errors.BadRequest(response_data, http_res)
@@ -967,7 +972,7 @@ class Tags(BaseSDK):
967
972
  server_url: Optional[str] = None,
968
973
  timeout_ms: Optional[int] = None,
969
974
  http_headers: Optional[Mapping[str, str]] = None,
970
- ) -> Optional[operations.DeleteTagResponseBody]:
975
+ ) -> operations.DeleteTagResponseBody:
971
976
  r"""Delete a tag
972
977
 
973
978
  Delete a tag from the workspace. All existing links will still work, but they will no longer be associated with this tag.
@@ -1005,6 +1010,7 @@ class Tags(BaseSDK):
1005
1010
  accept_header_value="application/json",
1006
1011
  http_headers=http_headers,
1007
1012
  security=self.sdk_configuration.security,
1013
+ allow_empty_value=None,
1008
1014
  timeout_ms=timeout_ms,
1009
1015
  )
1010
1016
 
@@ -1043,9 +1049,7 @@ class Tags(BaseSDK):
1043
1049
 
1044
1050
  response_data: Any = None
1045
1051
  if utils.match_response(http_res, "200", "application/json"):
1046
- return unmarshal_json_response(
1047
- Optional[operations.DeleteTagResponseBody], http_res
1048
- )
1052
+ return unmarshal_json_response(operations.DeleteTagResponseBody, http_res)
1049
1053
  if utils.match_response(http_res, "400", "application/json"):
1050
1054
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
1051
1055
  raise errors.BadRequest(response_data, http_res)
dub/track.py CHANGED
@@ -23,7 +23,7 @@ class Track(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
- ) -> Optional[operations.TrackLeadResponseBody]:
26
+ ) -> operations.TrackLeadResponseBody:
27
27
  r"""Track a lead
28
28
 
29
29
  Track a lead for a short link.
@@ -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
 
@@ -104,9 +105,7 @@ class Track(BaseSDK):
104
105
 
105
106
  response_data: Any = None
106
107
  if utils.match_response(http_res, "200", "application/json"):
107
- return unmarshal_json_response(
108
- Optional[operations.TrackLeadResponseBody], http_res
109
- )
108
+ return unmarshal_json_response(operations.TrackLeadResponseBody, http_res)
110
109
  if utils.match_response(http_res, "400", "application/json"):
111
110
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
112
111
  raise errors.BadRequest(response_data, http_res)
@@ -162,7 +161,7 @@ class Track(BaseSDK):
162
161
  server_url: Optional[str] = None,
163
162
  timeout_ms: Optional[int] = None,
164
163
  http_headers: Optional[Mapping[str, str]] = None,
165
- ) -> Optional[operations.TrackLeadResponseBody]:
164
+ ) -> operations.TrackLeadResponseBody:
166
165
  r"""Track a lead
167
166
 
168
167
  Track a lead for a short link.
@@ -205,6 +204,7 @@ class Track(BaseSDK):
205
204
  get_serialized_body=lambda: utils.serialize_request_body(
206
205
  request, False, True, "json", Optional[operations.TrackLeadRequestBody]
207
206
  ),
207
+ allow_empty_value=None,
208
208
  timeout_ms=timeout_ms,
209
209
  )
210
210
 
@@ -243,9 +243,7 @@ class Track(BaseSDK):
243
243
 
244
244
  response_data: Any = None
245
245
  if utils.match_response(http_res, "200", "application/json"):
246
- return unmarshal_json_response(
247
- Optional[operations.TrackLeadResponseBody], http_res
248
- )
246
+ return unmarshal_json_response(operations.TrackLeadResponseBody, http_res)
249
247
  if utils.match_response(http_res, "400", "application/json"):
250
248
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
251
249
  raise errors.BadRequest(response_data, http_res)
@@ -301,7 +299,7 @@ class Track(BaseSDK):
301
299
  server_url: Optional[str] = None,
302
300
  timeout_ms: Optional[int] = None,
303
301
  http_headers: Optional[Mapping[str, str]] = None,
304
- ) -> Optional[operations.TrackSaleResponseBody]:
302
+ ) -> operations.TrackSaleResponseBody:
305
303
  r"""Track a sale
306
304
 
307
305
  Track a sale for a short link.
@@ -344,6 +342,7 @@ class Track(BaseSDK):
344
342
  get_serialized_body=lambda: utils.serialize_request_body(
345
343
  request, False, True, "json", Optional[operations.TrackSaleRequestBody]
346
344
  ),
345
+ allow_empty_value=None,
347
346
  timeout_ms=timeout_ms,
348
347
  )
349
348
 
@@ -382,9 +381,7 @@ class Track(BaseSDK):
382
381
 
383
382
  response_data: Any = None
384
383
  if utils.match_response(http_res, "200", "application/json"):
385
- return unmarshal_json_response(
386
- Optional[operations.TrackSaleResponseBody], http_res
387
- )
384
+ return unmarshal_json_response(operations.TrackSaleResponseBody, http_res)
388
385
  if utils.match_response(http_res, "400", "application/json"):
389
386
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
390
387
  raise errors.BadRequest(response_data, http_res)
@@ -440,7 +437,7 @@ class Track(BaseSDK):
440
437
  server_url: Optional[str] = None,
441
438
  timeout_ms: Optional[int] = None,
442
439
  http_headers: Optional[Mapping[str, str]] = None,
443
- ) -> Optional[operations.TrackSaleResponseBody]:
440
+ ) -> operations.TrackSaleResponseBody:
444
441
  r"""Track a sale
445
442
 
446
443
  Track a sale for a short link.
@@ -483,6 +480,7 @@ class Track(BaseSDK):
483
480
  get_serialized_body=lambda: utils.serialize_request_body(
484
481
  request, False, True, "json", Optional[operations.TrackSaleRequestBody]
485
482
  ),
483
+ allow_empty_value=None,
486
484
  timeout_ms=timeout_ms,
487
485
  )
488
486
 
@@ -521,9 +519,7 @@ class Track(BaseSDK):
521
519
 
522
520
  response_data: Any = None
523
521
  if utils.match_response(http_res, "200", "application/json"):
524
- return unmarshal_json_response(
525
- Optional[operations.TrackSaleResponseBody], http_res
526
- )
522
+ return unmarshal_json_response(operations.TrackSaleResponseBody, http_res)
527
523
  if utils.match_response(http_res, "400", "application/json"):
528
524
  response_data = unmarshal_json_response(errors.BadRequestData, http_res)
529
525
  raise errors.BadRequest(response_data, http_res)
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