payi 0.1.0a110__py3-none-any.whl → 0.1.0a137__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 (83) hide show
  1. payi/__init__.py +3 -1
  2. payi/_base_client.py +12 -12
  3. payi/_client.py +8 -8
  4. payi/_compat.py +48 -48
  5. payi/_models.py +87 -59
  6. payi/_qs.py +7 -7
  7. payi/_streaming.py +4 -6
  8. payi/_types.py +53 -12
  9. payi/_utils/__init__.py +9 -2
  10. payi/_utils/_compat.py +45 -0
  11. payi/_utils/_datetime_parse.py +136 -0
  12. payi/_utils/_sync.py +3 -31
  13. payi/_utils/_transform.py +13 -3
  14. payi/_utils/_typing.py +6 -1
  15. payi/_utils/_utils.py +5 -6
  16. payi/_version.py +1 -1
  17. payi/lib/AnthropicInstrumentor.py +83 -57
  18. payi/lib/BedrockInstrumentor.py +292 -57
  19. payi/lib/GoogleGenAiInstrumentor.py +18 -31
  20. payi/lib/OpenAIInstrumentor.py +56 -72
  21. payi/lib/ProviderRequest.py +216 -0
  22. payi/lib/StreamWrappers.py +379 -0
  23. payi/lib/VertexInstrumentor.py +18 -37
  24. payi/lib/VertexRequest.py +16 -2
  25. payi/lib/data/cohere_embed_english_v3.json +30706 -0
  26. payi/lib/helpers.py +53 -1
  27. payi/lib/instrument.py +404 -668
  28. payi/resources/categories/__init__.py +0 -14
  29. payi/resources/categories/categories.py +25 -53
  30. payi/resources/categories/resources.py +27 -23
  31. payi/resources/ingest.py +126 -132
  32. payi/resources/limits/__init__.py +14 -14
  33. payi/resources/limits/limits.py +58 -58
  34. payi/resources/limits/properties.py +171 -0
  35. payi/resources/requests/request_id/properties.py +8 -8
  36. payi/resources/requests/request_id/result.py +3 -3
  37. payi/resources/requests/response_id/properties.py +8 -8
  38. payi/resources/requests/response_id/result.py +3 -3
  39. payi/resources/use_cases/definitions/definitions.py +27 -27
  40. payi/resources/use_cases/definitions/kpis.py +23 -23
  41. payi/resources/use_cases/definitions/limit_config.py +14 -14
  42. payi/resources/use_cases/definitions/version.py +3 -3
  43. payi/resources/use_cases/kpis.py +15 -15
  44. payi/resources/use_cases/properties.py +6 -6
  45. payi/resources/use_cases/use_cases.py +7 -7
  46. payi/types/__init__.py +2 -0
  47. payi/types/bulk_ingest_response.py +3 -20
  48. payi/types/categories/__init__.py +0 -1
  49. payi/types/categories/resource_list_params.py +5 -1
  50. payi/types/category_list_resources_params.py +5 -1
  51. payi/types/category_resource_response.py +31 -1
  52. payi/types/ingest_event_param.py +7 -6
  53. payi/types/ingest_units_params.py +5 -4
  54. payi/types/limit_create_params.py +3 -3
  55. payi/types/limit_list_response.py +1 -3
  56. payi/types/limit_response.py +1 -3
  57. payi/types/limits/__init__.py +2 -9
  58. payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
  59. payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
  60. payi/types/requests/request_id/property_update_params.py +2 -2
  61. payi/types/requests/response_id/property_update_params.py +2 -2
  62. payi/types/shared/__init__.py +2 -0
  63. payi/types/shared/api_error.py +18 -0
  64. payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  65. payi/types/shared/properties_request.py +11 -0
  66. payi/types/shared/xproxy_result.py +2 -0
  67. payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  68. payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
  69. payi/types/use_cases/property_update_params.py +2 -2
  70. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
  71. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
  72. payi/resources/categories/fixed_cost_resources.py +0 -196
  73. payi/resources/limits/tags.py +0 -507
  74. payi/types/categories/fixed_cost_resource_create_params.py +0 -21
  75. payi/types/limits/limit_tags.py +0 -16
  76. payi/types/limits/tag_create_params.py +0 -13
  77. payi/types/limits/tag_create_response.py +0 -10
  78. payi/types/limits/tag_list_response.py +0 -10
  79. payi/types/limits/tag_remove_response.py +0 -10
  80. payi/types/limits/tag_update_params.py +0 -13
  81. payi/types/limits/tag_update_response.py +0 -10
  82. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
  83. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/licenses/LICENSE +0 -0
payi/_models.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import inspect
5
+ import weakref
5
6
  from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
6
7
  from datetime import date, datetime
7
8
  from typing_extensions import (
@@ -50,7 +51,7 @@ from ._utils import (
50
51
  strip_annotated_type,
51
52
  )
52
53
  from ._compat import (
53
- PYDANTIC_V2,
54
+ PYDANTIC_V1,
54
55
  ConfigDict,
55
56
  GenericModel as BaseGenericModel,
56
57
  get_args,
@@ -81,11 +82,7 @@ class _ConfigProtocol(Protocol):
81
82
 
82
83
 
83
84
  class BaseModel(pydantic.BaseModel):
84
- if PYDANTIC_V2:
85
- model_config: ClassVar[ConfigDict] = ConfigDict(
86
- extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
87
- )
88
- else:
85
+ if PYDANTIC_V1:
89
86
 
90
87
  @property
91
88
  @override
@@ -95,6 +92,10 @@ class BaseModel(pydantic.BaseModel):
95
92
 
96
93
  class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
97
94
  extra: Any = pydantic.Extra.allow # type: ignore
95
+ else:
96
+ model_config: ClassVar[ConfigDict] = ConfigDict(
97
+ extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
98
+ )
98
99
 
99
100
  def to_dict(
100
101
  self,
@@ -215,25 +216,25 @@ class BaseModel(pydantic.BaseModel):
215
216
  if key not in model_fields:
216
217
  parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
217
218
 
218
- if PYDANTIC_V2:
219
- _extra[key] = parsed
220
- else:
219
+ if PYDANTIC_V1:
221
220
  _fields_set.add(key)
222
221
  fields_values[key] = parsed
222
+ else:
223
+ _extra[key] = parsed
223
224
 
224
225
  object.__setattr__(m, "__dict__", fields_values)
225
226
 
226
- if PYDANTIC_V2:
227
- # these properties are copied from Pydantic's `model_construct()` method
228
- object.__setattr__(m, "__pydantic_private__", None)
229
- object.__setattr__(m, "__pydantic_extra__", _extra)
230
- object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
231
- else:
227
+ if PYDANTIC_V1:
232
228
  # init_private_attributes() does not exist in v2
233
229
  m._init_private_attributes() # type: ignore
234
230
 
235
231
  # copied from Pydantic v1's `construct()` method
236
232
  object.__setattr__(m, "__fields_set__", _fields_set)
233
+ else:
234
+ # these properties are copied from Pydantic's `model_construct()` method
235
+ object.__setattr__(m, "__pydantic_private__", None)
236
+ object.__setattr__(m, "__pydantic_extra__", _extra)
237
+ object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
237
238
 
238
239
  return m
239
240
 
@@ -243,7 +244,7 @@ class BaseModel(pydantic.BaseModel):
243
244
  # although not in practice
244
245
  model_construct = construct
245
246
 
246
- if not PYDANTIC_V2:
247
+ if PYDANTIC_V1:
247
248
  # we define aliases for some of the new pydantic v2 methods so
248
249
  # that we can just document these methods without having to specify
249
250
  # a specific pydantic version as some users may not know which
@@ -256,13 +257,15 @@ class BaseModel(pydantic.BaseModel):
256
257
  mode: Literal["json", "python"] | str = "python",
257
258
  include: IncEx | None = None,
258
259
  exclude: IncEx | None = None,
259
- by_alias: bool = False,
260
+ context: Any | None = None,
261
+ by_alias: bool | None = None,
260
262
  exclude_unset: bool = False,
261
263
  exclude_defaults: bool = False,
262
264
  exclude_none: bool = False,
265
+ exclude_computed_fields: bool = False,
263
266
  round_trip: bool = False,
264
267
  warnings: bool | Literal["none", "warn", "error"] = True,
265
- context: dict[str, Any] | None = None,
268
+ fallback: Callable[[Any], Any] | None = None,
266
269
  serialize_as_any: bool = False,
267
270
  ) -> dict[str, Any]:
268
271
  """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -271,16 +274,24 @@ class BaseModel(pydantic.BaseModel):
271
274
 
272
275
  Args:
273
276
  mode: The mode in which `to_python` should run.
274
- If mode is 'json', the dictionary will only contain JSON serializable types.
275
- If mode is 'python', the dictionary may contain any Python objects.
276
- include: A list of fields to include in the output.
277
- exclude: A list of fields to exclude from the output.
277
+ If mode is 'json', the output will only contain JSON serializable types.
278
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
279
+ include: A set of fields to include in the output.
280
+ exclude: A set of fields to exclude from the output.
281
+ context: Additional context to pass to the serializer.
278
282
  by_alias: Whether to use the field's alias in the dictionary key if defined.
279
- exclude_unset: Whether to exclude fields that are unset or None from the output.
280
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
281
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
282
- round_trip: Whether to enable serialization and deserialization round-trip support.
283
- warnings: Whether to log warnings when invalid fields are encountered.
283
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
284
+ exclude_defaults: Whether to exclude fields that are set to their default value.
285
+ exclude_none: Whether to exclude fields that have a value of `None`.
286
+ exclude_computed_fields: Whether to exclude computed fields.
287
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
288
+ `round_trip` parameter instead.
289
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
290
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
291
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
292
+ fallback: A function to call when an unknown value is encountered. If not provided,
293
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
294
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
284
295
 
285
296
  Returns:
286
297
  A dictionary representation of the model.
@@ -295,31 +306,38 @@ class BaseModel(pydantic.BaseModel):
295
306
  raise ValueError("context is only supported in Pydantic v2")
296
307
  if serialize_as_any != False:
297
308
  raise ValueError("serialize_as_any is only supported in Pydantic v2")
309
+ if fallback is not None:
310
+ raise ValueError("fallback is only supported in Pydantic v2")
311
+ if exclude_computed_fields != False:
312
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
298
313
  dumped = super().dict( # pyright: ignore[reportDeprecated]
299
314
  include=include,
300
315
  exclude=exclude,
301
- by_alias=by_alias,
316
+ by_alias=by_alias if by_alias is not None else False,
302
317
  exclude_unset=exclude_unset,
303
318
  exclude_defaults=exclude_defaults,
304
319
  exclude_none=exclude_none,
305
320
  )
306
321
 
307
- return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
322
+ return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
308
323
 
309
324
  @override
310
325
  def model_dump_json(
311
326
  self,
312
327
  *,
313
328
  indent: int | None = None,
329
+ ensure_ascii: bool = False,
314
330
  include: IncEx | None = None,
315
331
  exclude: IncEx | None = None,
316
- by_alias: bool = False,
332
+ context: Any | None = None,
333
+ by_alias: bool | None = None,
317
334
  exclude_unset: bool = False,
318
335
  exclude_defaults: bool = False,
319
336
  exclude_none: bool = False,
337
+ exclude_computed_fields: bool = False,
320
338
  round_trip: bool = False,
321
339
  warnings: bool | Literal["none", "warn", "error"] = True,
322
- context: dict[str, Any] | None = None,
340
+ fallback: Callable[[Any], Any] | None = None,
323
341
  serialize_as_any: bool = False,
324
342
  ) -> str:
325
343
  """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
@@ -348,11 +366,17 @@ class BaseModel(pydantic.BaseModel):
348
366
  raise ValueError("context is only supported in Pydantic v2")
349
367
  if serialize_as_any != False:
350
368
  raise ValueError("serialize_as_any is only supported in Pydantic v2")
369
+ if fallback is not None:
370
+ raise ValueError("fallback is only supported in Pydantic v2")
371
+ if ensure_ascii != False:
372
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
373
+ if exclude_computed_fields != False:
374
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
351
375
  return super().json( # type: ignore[reportDeprecated]
352
376
  indent=indent,
353
377
  include=include,
354
378
  exclude=exclude,
355
- by_alias=by_alias,
379
+ by_alias=by_alias if by_alias is not None else False,
356
380
  exclude_unset=exclude_unset,
357
381
  exclude_defaults=exclude_defaults,
358
382
  exclude_none=exclude_none,
@@ -363,10 +387,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
363
387
  if value is None:
364
388
  return field_get_default(field)
365
389
 
366
- if PYDANTIC_V2:
367
- type_ = field.annotation
368
- else:
390
+ if PYDANTIC_V1:
369
391
  type_ = cast(type, field.outer_type_) # type: ignore
392
+ else:
393
+ type_ = field.annotation # type: ignore
370
394
 
371
395
  if type_ is None:
372
396
  raise RuntimeError(f"Unexpected field type is None for {key}")
@@ -375,7 +399,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
375
399
 
376
400
 
377
401
  def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
378
- if not PYDANTIC_V2:
402
+ if PYDANTIC_V1:
379
403
  # TODO
380
404
  return None
381
405
 
@@ -567,6 +591,9 @@ class CachedDiscriminatorType(Protocol):
567
591
  __discriminator__: DiscriminatorDetails
568
592
 
569
593
 
594
+ DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
595
+
596
+
570
597
  class DiscriminatorDetails:
571
598
  field_name: str
572
599
  """The name of the discriminator field in the variant class, e.g.
@@ -609,8 +636,9 @@ class DiscriminatorDetails:
609
636
 
610
637
 
611
638
  def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
612
- if isinstance(union, CachedDiscriminatorType):
613
- return union.__discriminator__
639
+ cached = DISCRIMINATOR_CACHE.get(union)
640
+ if cached is not None:
641
+ return cached
614
642
 
615
643
  discriminator_field_name: str | None = None
616
644
 
@@ -628,30 +656,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
628
656
  for variant in get_args(union):
629
657
  variant = strip_annotated_type(variant)
630
658
  if is_basemodel_type(variant):
631
- if PYDANTIC_V2:
632
- field = _extract_field_schema_pv2(variant, discriminator_field_name)
633
- if not field:
659
+ if PYDANTIC_V1:
660
+ field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
661
+ if not field_info:
634
662
  continue
635
663
 
636
664
  # Note: if one variant defines an alias then they all should
637
- discriminator_alias = field.get("serialization_alias")
638
-
639
- field_schema = field["schema"]
665
+ discriminator_alias = field_info.alias
640
666
 
641
- if field_schema["type"] == "literal":
642
- for entry in cast("LiteralSchema", field_schema)["expected"]:
667
+ if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
668
+ for entry in get_args(annotation):
643
669
  if isinstance(entry, str):
644
670
  mapping[entry] = variant
645
671
  else:
646
- field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
647
- if not field_info:
672
+ field = _extract_field_schema_pv2(variant, discriminator_field_name)
673
+ if not field:
648
674
  continue
649
675
 
650
676
  # Note: if one variant defines an alias then they all should
651
- discriminator_alias = field_info.alias
677
+ discriminator_alias = field.get("serialization_alias")
652
678
 
653
- if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
654
- for entry in get_args(annotation):
679
+ field_schema = field["schema"]
680
+
681
+ if field_schema["type"] == "literal":
682
+ for entry in cast("LiteralSchema", field_schema)["expected"]:
655
683
  if isinstance(entry, str):
656
684
  mapping[entry] = variant
657
685
 
@@ -663,7 +691,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
663
691
  discriminator_field=discriminator_field_name,
664
692
  discriminator_alias=discriminator_alias,
665
693
  )
666
- cast(CachedDiscriminatorType, union).__discriminator__ = details
694
+ DISCRIMINATOR_CACHE.setdefault(union, details)
667
695
  return details
668
696
 
669
697
 
@@ -714,7 +742,7 @@ else:
714
742
  pass
715
743
 
716
744
 
717
- if PYDANTIC_V2:
745
+ if not PYDANTIC_V1:
718
746
  from pydantic import TypeAdapter as _TypeAdapter
719
747
 
720
748
  _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
@@ -782,12 +810,12 @@ class FinalRequestOptions(pydantic.BaseModel):
782
810
  json_data: Union[Body, None] = None
783
811
  extra_json: Union[AnyMapping, None] = None
784
812
 
785
- if PYDANTIC_V2:
786
- model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
787
- else:
813
+ if PYDANTIC_V1:
788
814
 
789
815
  class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
790
816
  arbitrary_types_allowed: bool = True
817
+ else:
818
+ model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
791
819
 
792
820
  def get_max_retries(self, max_retries: int) -> int:
793
821
  if isinstance(self.max_retries, NotGiven):
@@ -820,9 +848,9 @@ class FinalRequestOptions(pydantic.BaseModel):
820
848
  key: strip_not_given(value)
821
849
  for key, value in values.items()
822
850
  }
823
- if PYDANTIC_V2:
824
- return super().model_construct(_fields_set, **kwargs)
825
- return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
851
+ if PYDANTIC_V1:
852
+ return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
853
+ return super().model_construct(_fields_set, **kwargs)
826
854
 
827
855
  if not TYPE_CHECKING:
828
856
  # type checkers incorrectly complain about this assignment
payi/_qs.py CHANGED
@@ -4,7 +4,7 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar
4
4
  from urllib.parse import parse_qs, urlencode
5
5
  from typing_extensions import Literal, get_args
6
6
 
7
- from ._types import NOT_GIVEN, NotGiven, NotGivenOr
7
+ from ._types import NotGiven, not_given
8
8
  from ._utils import flatten
9
9
 
10
10
  _T = TypeVar("_T")
@@ -41,8 +41,8 @@ class Querystring:
41
41
  self,
42
42
  params: Params,
43
43
  *,
44
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
45
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
44
+ array_format: ArrayFormat | NotGiven = not_given,
45
+ nested_format: NestedFormat | NotGiven = not_given,
46
46
  ) -> str:
47
47
  return urlencode(
48
48
  self.stringify_items(
@@ -56,8 +56,8 @@ class Querystring:
56
56
  self,
57
57
  params: Params,
58
58
  *,
59
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
60
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
59
+ array_format: ArrayFormat | NotGiven = not_given,
60
+ nested_format: NestedFormat | NotGiven = not_given,
61
61
  ) -> list[tuple[str, str]]:
62
62
  opts = Options(
63
63
  qs=self,
@@ -143,8 +143,8 @@ class Options:
143
143
  self,
144
144
  qs: Querystring = _qs,
145
145
  *,
146
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
147
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
146
+ array_format: ArrayFormat | NotGiven = not_given,
147
+ nested_format: NestedFormat | NotGiven = not_given,
148
148
  ) -> None:
149
149
  self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format
150
150
  self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format
payi/_streaming.py CHANGED
@@ -57,9 +57,8 @@ class Stream(Generic[_T]):
57
57
  for sse in iterator:
58
58
  yield process_data(data=sse.json(), cast_to=cast_to, response=response)
59
59
 
60
- # Ensure the entire stream is consumed
61
- for _sse in iterator:
62
- ...
60
+ # As we might not fully consume the response stream, we need to close it explicitly
61
+ response.close()
63
62
 
64
63
  def __enter__(self) -> Self:
65
64
  return self
@@ -121,9 +120,8 @@ class AsyncStream(Generic[_T]):
121
120
  async for sse in iterator:
122
121
  yield process_data(data=sse.json(), cast_to=cast_to, response=response)
123
122
 
124
- # Ensure the entire stream is consumed
125
- async for _sse in iterator:
126
- ...
123
+ # As we might not fully consume the response stream, we need to close it explicitly
124
+ await response.aclose()
127
125
 
128
126
  async def __aenter__(self) -> Self:
129
127
  return self
payi/_types.py CHANGED
@@ -13,10 +13,21 @@ from typing import (
13
13
  Mapping,
14
14
  TypeVar,
15
15
  Callable,
16
+ Iterator,
16
17
  Optional,
17
18
  Sequence,
18
19
  )
19
- from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
20
+ from typing_extensions import (
21
+ Set,
22
+ Literal,
23
+ Protocol,
24
+ TypeAlias,
25
+ TypedDict,
26
+ SupportsIndex,
27
+ overload,
28
+ override,
29
+ runtime_checkable,
30
+ )
20
31
 
21
32
  import httpx
22
33
  import pydantic
@@ -106,18 +117,21 @@ class RequestOptions(TypedDict, total=False):
106
117
  # Sentinel class used until PEP 0661 is accepted
107
118
  class NotGiven:
108
119
  """
109
- A sentinel singleton class used to distinguish omitted keyword arguments
110
- from those passed in with the value None (which may have different behavior).
120
+ For parameters with a meaningful None value, we need to distinguish between
121
+ the user explicitly passing None, and the user not passing the parameter at
122
+ all.
123
+
124
+ User code shouldn't need to use not_given directly.
111
125
 
112
126
  For example:
113
127
 
114
128
  ```py
115
- def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ...
129
+ def create(timeout: Timeout | None | NotGiven = not_given): ...
116
130
 
117
131
 
118
- get(timeout=1) # 1s timeout
119
- get(timeout=None) # No timeout
120
- get() # Default timeout behavior, which may not be statically known at the method definition.
132
+ create(timeout=1) # 1s timeout
133
+ create(timeout=None) # No timeout
134
+ create() # Default timeout behavior
121
135
  ```
122
136
  """
123
137
 
@@ -129,13 +143,14 @@ class NotGiven:
129
143
  return "NOT_GIVEN"
130
144
 
131
145
 
132
- NotGivenOr = Union[_T, NotGiven]
146
+ not_given = NotGiven()
147
+ # for backwards compatibility:
133
148
  NOT_GIVEN = NotGiven()
134
149
 
135
150
 
136
151
  class Omit:
137
- """In certain situations you need to be able to represent a case where a default value has
138
- to be explicitly removed and `None` is not an appropriate substitute, for example:
152
+ """
153
+ To explicitly omit something from being sent in a request, use `omit`.
139
154
 
140
155
  ```py
141
156
  # as the default `Content-Type` header is `application/json` that will be sent
@@ -145,8 +160,8 @@ class Omit:
145
160
  # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
146
161
  client.post(..., headers={"Content-Type": "multipart/form-data"})
147
162
 
148
- # instead you can remove the default `application/json` header by passing Omit
149
- client.post(..., headers={"Content-Type": Omit()})
163
+ # instead you can remove the default `application/json` header by passing omit
164
+ client.post(..., headers={"Content-Type": omit})
150
165
  ```
151
166
  """
152
167
 
@@ -154,6 +169,9 @@ class Omit:
154
169
  return False
155
170
 
156
171
 
172
+ omit = Omit()
173
+
174
+
157
175
  @runtime_checkable
158
176
  class ModelBuilderProtocol(Protocol):
159
177
  @classmethod
@@ -217,3 +235,26 @@ class _GenericAlias(Protocol):
217
235
  class HttpxSendArgs(TypedDict, total=False):
218
236
  auth: httpx.Auth
219
237
  follow_redirects: bool
238
+
239
+
240
+ _T_co = TypeVar("_T_co", covariant=True)
241
+
242
+
243
+ if TYPE_CHECKING:
244
+ # This works because str.__contains__ does not accept object (either in typeshed or at runtime)
245
+ # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
246
+ class SequenceNotStr(Protocol[_T_co]):
247
+ @overload
248
+ def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
249
+ @overload
250
+ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
251
+ def __contains__(self, value: object, /) -> bool: ...
252
+ def __len__(self) -> int: ...
253
+ def __iter__(self) -> Iterator[_T_co]: ...
254
+ def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
255
+ def count(self, value: Any, /) -> int: ...
256
+ def __reversed__(self) -> Iterator[_T_co]: ...
257
+ else:
258
+ # just point this to a normal `Sequence` at runtime to avoid having to special case
259
+ # deserializing our custom sequence type
260
+ SequenceNotStr = Sequence
payi/_utils/__init__.py CHANGED
@@ -10,7 +10,6 @@ from ._utils import (
10
10
  lru_cache as lru_cache,
11
11
  is_mapping as is_mapping,
12
12
  is_tuple_t as is_tuple_t,
13
- parse_date as parse_date,
14
13
  is_iterable as is_iterable,
15
14
  is_sequence as is_sequence,
16
15
  coerce_float as coerce_float,
@@ -23,7 +22,6 @@ from ._utils import (
23
22
  coerce_boolean as coerce_boolean,
24
23
  coerce_integer as coerce_integer,
25
24
  file_from_path as file_from_path,
26
- parse_datetime as parse_datetime,
27
25
  strip_not_given as strip_not_given,
28
26
  deepcopy_minimal as deepcopy_minimal,
29
27
  get_async_library as get_async_library,
@@ -32,12 +30,20 @@ from ._utils import (
32
30
  maybe_coerce_boolean as maybe_coerce_boolean,
33
31
  maybe_coerce_integer as maybe_coerce_integer,
34
32
  )
33
+ from ._compat import (
34
+ get_args as get_args,
35
+ is_union as is_union,
36
+ get_origin as get_origin,
37
+ is_typeddict as is_typeddict,
38
+ is_literal_type as is_literal_type,
39
+ )
35
40
  from ._typing import (
36
41
  is_list_type as is_list_type,
37
42
  is_union_type as is_union_type,
38
43
  extract_type_arg as extract_type_arg,
39
44
  is_iterable_type as is_iterable_type,
40
45
  is_required_type as is_required_type,
46
+ is_sequence_type as is_sequence_type,
41
47
  is_annotated_type as is_annotated_type,
42
48
  is_type_alias_type as is_type_alias_type,
43
49
  strip_annotated_type as strip_annotated_type,
@@ -55,3 +61,4 @@ from ._reflection import (
55
61
  function_has_argument as function_has_argument,
56
62
  assert_signatures_in_sync as assert_signatures_in_sync,
57
63
  )
64
+ from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
payi/_utils/_compat.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing_extensions
5
+ from typing import Any, Type, Union, Literal, Optional
6
+ from datetime import date, datetime
7
+ from typing_extensions import get_args as _get_args, get_origin as _get_origin
8
+
9
+ from .._types import StrBytesIntFloat
10
+ from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime
11
+
12
+ _LITERAL_TYPES = {Literal, typing_extensions.Literal}
13
+
14
+
15
+ def get_args(tp: type[Any]) -> tuple[Any, ...]:
16
+ return _get_args(tp)
17
+
18
+
19
+ def get_origin(tp: type[Any]) -> type[Any] | None:
20
+ return _get_origin(tp)
21
+
22
+
23
+ def is_union(tp: Optional[Type[Any]]) -> bool:
24
+ if sys.version_info < (3, 10):
25
+ return tp is Union # type: ignore[comparison-overlap]
26
+ else:
27
+ import types
28
+
29
+ return tp is Union or tp is types.UnionType
30
+
31
+
32
+ def is_typeddict(tp: Type[Any]) -> bool:
33
+ return typing_extensions.is_typeddict(tp)
34
+
35
+
36
+ def is_literal_type(tp: Type[Any]) -> bool:
37
+ return get_origin(tp) in _LITERAL_TYPES
38
+
39
+
40
+ def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
41
+ return _parse_date(value)
42
+
43
+
44
+ def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
45
+ return _parse_datetime(value)