ul-api-utils 9.0.0a6__py3-none-any.whl → 9.0.0a7__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.

Potentially problematic release.


This version of ul-api-utils might be problematic. Click here for more details.

@@ -16,8 +16,9 @@ from ul_api_utils.api_resource.api_resource_error_handling import WEB_EXCEPTION_
16
16
  ProcessingExceptionsParams, WEB_UNKNOWN_ERROR_PARAMS
17
17
  from ul_api_utils.api_resource.api_resource_fn_typing import ApiResourceFnTyping
18
18
  from ul_api_utils.api_resource.api_resource_type import ApiResourceType
19
- from ul_api_utils.api_resource.api_response import JsonApiResponse, HtmlApiResponse, FileApiResponse, JsonApiResponsePayload, EmptyJsonApiResponse, \
20
- RedirectApiResponse, ProxyJsonApiResponse, RootJsonApiResponse
19
+ from ul_api_utils.api_resource.api_response import JsonApiResponse, HtmlApiResponse, FileApiResponse, \
20
+ JsonApiResponsePayload, EmptyJsonApiResponse, \
21
+ RedirectApiResponse, ProxyJsonApiResponse, RootJsonApiResponse, RootJsonApiResponsePayload
21
22
  from ul_api_utils.conf import APPLICATION_ENV, APPLICATION_JWT_PUBLIC_KEY, APPLICATION_DEBUG
22
23
  from ul_api_utils.const import REQUEST_HEADER__X_FORWARDED_FOR, \
23
24
  RESPONSE_HEADER__WWW_AUTH, OOPS, REQUEST_HEADER__USER_AGENT
@@ -38,7 +39,7 @@ TPayload = TypeVar('TPayload')
38
39
 
39
40
 
40
41
  T = TypeVar('T')
41
- TResp = TypeVar('TResp', bound=JsonApiResponsePayload)
42
+ TResp = TypeVar('TResp', bound=Union[JsonApiResponsePayload, RootJsonApiResponsePayload[Any]])
42
43
 
43
44
 
44
45
  class ApiResource:
@@ -2,7 +2,7 @@ import inspect
2
2
  from typing import NamedTuple, Any, Callable, Optional, List, Dict, Type, Tuple, TYPE_CHECKING, Union, get_origin, get_args
3
3
 
4
4
  from flask import request
5
- from pydantic import BaseModel, ValidationError, validate_call, TypeAdapter
5
+ from pydantic import BaseModel, ValidationError, validate_call, TypeAdapter, RootModel
6
6
  from pydantic.v1.utils import deep_update
7
7
  from pydantic_core import ErrorDetails
8
8
 
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
25
25
  FN_SYSTEM_PROPS = {"api_resource", "query", "body", "return", "body_validation_error", "query_validation_error"}
26
26
 
27
27
 
28
- def _is_complex_type(annotation):
28
+ def _is_complex_type(annotation: Any) -> bool:
29
29
  origin = get_origin(annotation)
30
30
 
31
31
  # Optional[type_] is typing.Union
@@ -82,18 +82,18 @@ class ApiResourceFnTyping(NamedTuple):
82
82
 
83
83
  if self.request_body_optional:
84
84
  if self.request_body_many:
85
- class BodyTypingOptList(BaseModel):
86
- __root__: Optional[List[body_typing]] = None # type: ignore
85
+ class BodyTypingOptList(RootModel[List[body_typing] | None]): # type: ignore
86
+ pass
87
87
 
88
88
  return BodyTypingOptList
89
89
 
90
- class BodyTypingOpt(BaseModel):
91
- __root__: Optional[body_typing] = None # type: ignore
90
+ class BodyTypingOpt(RootModel[Optional[body_typing]]): # type: ignore
91
+ pass
92
92
  return BodyTypingOpt
93
93
 
94
94
  if self.request_body_many:
95
- class BodyTypingList(BaseModel):
96
- __root__: List[body_typing] # type: ignore
95
+ class BodyTypingList(RootModel[List[body_typing]]): # type: ignore
96
+ pass
97
97
 
98
98
  return BodyTypingList
99
99
  return body_typing
@@ -165,8 +165,7 @@ class ApiResourceFnTyping(NamedTuple):
165
165
  return kwargs, errors
166
166
  body = self._get_body()
167
167
  try:
168
- expected_typing: Type[BaseModel | List[BaseModel]] = List[self.body_typing] \
169
- if self.request_body_many else self.body_typing
168
+ expected_typing: Type[BaseModel | List[BaseModel]] = List[self.body_typing] if self.request_body_many else self.body_typing # type: ignore
170
169
  kwargs['body'] = TypeAdapter(expected_typing).validate_python(body)
171
170
  except ValidationError as ve:
172
171
  if self.has_body_validation_error:
@@ -1,6 +1,5 @@
1
1
  import io
2
2
  from datetime import datetime
3
- from types import NoneType
4
3
  from typing import TypeVar, Generic, List, Optional, Dict, Any, Callable, Union, BinaryIO, Tuple, Type
5
4
 
6
5
  import msgpack
@@ -64,8 +63,8 @@ class HtmlApiResponse(ApiResponse):
64
63
 
65
64
  @classmethod
66
65
  def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
67
- class _ResponseNoneType(BaseModel):
68
- __root__: NoneType # type: ignore
66
+ class _ResponseNoneType(RootModel[None]):
67
+ pass
69
68
  return _ResponseNoneType
70
69
 
71
70
  def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
@@ -100,8 +99,8 @@ class FileApiResponse(ApiResponse):
100
99
 
101
100
  @classmethod
102
101
  def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
103
- class _ResponseNoneType(BaseModel):
104
- __root__: NoneType # type: ignore
102
+ class _ResponseNoneType(RootModel[None]):
103
+ pass
105
104
  return _ResponseNoneType
106
105
 
107
106
  def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
@@ -125,8 +124,8 @@ class EmptyJsonApiResponse(ApiResponse):
125
124
 
126
125
  @classmethod
127
126
  def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
128
- class _ResponseNoneType(BaseModel):
129
- __root__: NoneType # type: ignore
127
+ class _ResponseNoneType(RootModel[None]):
128
+ pass
130
129
  return _ResponseNoneType
131
130
 
132
131
  def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
@@ -162,7 +161,7 @@ class DictJsonApiResponsePayload(RootJsonApiResponsePayload[Dict[str, Any]]):
162
161
  pass
163
162
 
164
163
 
165
- TProxyPayload = TypeVar('TProxyPayload', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], None])
164
+ TProxyPayload = TypeVar('TProxyPayload', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]], None])
166
165
 
167
166
 
168
167
  class ProxyJsonApiResponse(Generic[TProxyPayload], EmptyJsonApiResponse):
@@ -193,8 +192,8 @@ class RootJsonApiResponse(Generic[TJsonObjApiResponsePayload], EmptyJsonApiRespo
193
192
 
194
193
  @classmethod
195
194
  def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
196
- class _ResponseStd(BaseModel):
197
- __root__: inner_type # type: ignore
195
+ class _ResponseStd(RootModel[inner_type]): # type: ignore
196
+ pass
198
197
  return _ResponseStd
199
198
 
200
199
  def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
@@ -79,7 +79,7 @@ class Debugger:
79
79
  return
80
80
 
81
81
  started_at = stat.get_request_started_at()
82
- ended_at = time.time()
82
+ ended_at = time.perf_counter()
83
83
  name = self.name
84
84
 
85
85
  stats = stat.get_stat(code_spans=True, started_at=started_at, ended_at=ended_at)
@@ -90,7 +90,7 @@ class Debugger:
90
90
  def render_dict(self, status_code: int) -> Dict[str, Any]:
91
91
  if not collecting_enabled():
92
92
  return {}
93
- stats = [st.unwrap() for st in stat.get_stat(started_at=stat.get_request_started_at(), ended_at=time.time())]
93
+ stats = [st.unwrap() for st in stat.get_stat(started_at=stat.get_request_started_at(), ended_at=time.perf_counter())]
94
94
 
95
95
  return {
96
96
  RESPONSE_PROP_DEBUG_STATS: stats,
@@ -107,7 +107,7 @@ class Debugger:
107
107
  return script
108
108
 
109
109
  started_at = stat.get_request_started_at()
110
- ended_at = time.time()
110
+ ended_at = time.perf_counter()
111
111
 
112
112
  stats = stat.get_stat(code_spans=True, started_at=started_at, ended_at=ended_at)
113
113
 
@@ -18,7 +18,7 @@ INDENT = ' '
18
18
 
19
19
 
20
20
  def time_now() -> float:
21
- return time.time()
21
+ return time.perf_counter()
22
22
 
23
23
 
24
24
  def mark_request_started() -> None:
@@ -439,11 +439,6 @@ def mk_stat_string(
439
439
  header = f'{IND}{" " * (i_size * 2 + 2)}{IND}Request{" " * MAX_LEN_OF_TYPE}{INDENT * max_lvl}{col_duration(started_at, ended_at, cm=cm)}{INDENT}{INDENT}{total_str}'
440
440
 
441
441
  short_identifier = uuid.uuid4().hex[:8]
442
- started_dt = datetime.fromtimestamp(started_at)
443
- ended_dt = datetime.fromtimestamp(ended_at)
444
- date_str = f'{cm[C_FG_GRAY]}{started_dt.date().isoformat()}{cm[C_NC]}' \
445
- f'{IND}{started_dt.strftime("%H:%M:%S")}{cm[C_FG_GRAY]}.{started_dt.microsecond // 1000:0<3}{cm[C_NC]}' \
446
- f' - {ended_dt.strftime("%H:%M:%S")}{cm[C_FG_GRAY]}.{ended_dt.microsecond // 1000:0<3}{cm[C_NC]}'
447
- return f'▀▀▀▀▀ {name} {cm[C_FG_GRAY]}{short_identifier}{cm[C_NC]}{IND}{date_str} {"▀" * 55}' \
442
+ return f'▀▀▀▀▀ {name} {cm[C_FG_GRAY]}{short_identifier}{cm[C_NC]}{IND}{"▀" * 55}' \
448
443
  f'\n{header}\n' \
449
444
  f'{result_s}▄▄▄▄▄ {name} {cm[C_FG_GRAY]}{short_identifier}{cm[C_NC]} {"▄" * 96}'
@@ -278,7 +278,7 @@ class InternalApi:
278
278
  ):
279
279
  return self._override[path][method](has_std_schema)
280
280
 
281
- started_at = time.time()
281
+ started_at = time.perf_counter()
282
282
  q = ApiPathVersion.cleanup_q(q)
283
283
  url = f'{self._entry_point.rstrip("/")}{path}'
284
284
 
@@ -7,7 +7,7 @@ from typing import Any, Optional, List, Generic, Type, TypeVar, Union, Dict, Nam
7
7
  import requests
8
8
  from pydantic import ValidationError
9
9
 
10
- from ul_api_utils.api_resource.api_response import JsonApiResponsePayload
10
+ from ul_api_utils.api_resource.api_response import JsonApiResponsePayload, RootJsonApiResponsePayload
11
11
  from ul_api_utils.api_resource.signature_check import set_model
12
12
  from ul_api_utils.const import RESPONSE_PROP_DEBUG_STATS, RESPONSE_PROP_PAYLOAD, RESPONSE_PROP_OK, RESPONSE_PROP_ERRORS, RESPONSE_PROP_TOTAL, RESPONSE_PROP_COUNT, MIME__JSON, \
13
13
  RESPONSE_HEADER__CONTENT_TYPE
@@ -18,7 +18,7 @@ from ul_api_utils.utils.api_format import ApiFormat
18
18
  from ul_api_utils.internal_api.internal_api_error import InternalApiResponseErrorObj
19
19
  from ul_api_utils.utils.unwrap_typing import UnwrappedOptionalObjOrListOfObj
20
20
 
21
- TPyloadType = TypeVar('TPyloadType', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], None])
21
+ TPyloadType = TypeVar('TPyloadType', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]], None])
22
22
  TResp = TypeVar('TResp', bound='InternalApiResponse[Any]')
23
23
 
24
24
 
@@ -73,17 +73,17 @@ class InternalApiResponse(Generic[TPyloadType]):
73
73
 
74
74
  self._parsed_schema: Optional[InternalApiResponseSchema] = None
75
75
  self._parsed_json: Any = None
76
- self._parsed_payload: Union[None, JsonApiResponsePayload, List[JsonApiResponsePayload]] = None
76
+ self._parsed_payload: Union[None, JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]]] = None
77
77
 
78
78
  self._status_checked = False
79
79
  self._info = info
80
80
 
81
81
  if payload_type is not None:
82
- self._payload_type = UnwrappedOptionalObjOrListOfObj.parse(payload_type, JsonApiResponsePayload)
82
+ self._payload_type = UnwrappedOptionalObjOrListOfObj.parse(payload_type, JsonApiResponsePayload) or UnwrappedOptionalObjOrListOfObj.parse(payload_type, RootJsonApiResponsePayload)
83
83
  if self._payload_type is None:
84
- tn = JsonApiResponsePayload.__name__
84
+ tn1, tn2 = JsonApiResponsePayload.__name__, RootJsonApiResponsePayload.__name__
85
85
  raise ValueError(
86
- f'payload_typing is invalid. must be Union[Type[{tn}], Optional[Type[{tn}]], List[Type[{tn}]], Optional[List[Type[{tn}]]]]. {payload_type} was given',
86
+ f'payload_typing is invalid. must be Union[Type[{tn1} | {tn2}], Optional[Type[{tn1} | {tn2}]], List[Type[{tn1} | {tn2}]], Optional[List[Type[{tn1} | {tn2}]]]]. {payload_type} was given',
87
87
  )
88
88
 
89
89
  def typed(self, payload_type: Type[TPyloadType]) -> 'InternalApiResponse[TPyloadType]':
@@ -117,7 +117,7 @@ class ApiSdk:
117
117
 
118
118
  self._config = config
119
119
  self._routes_loaded = False
120
- self._request_started_at = time.time()
120
+ self._request_started_at = time.perf_counter()
121
121
  self._initialized_flask_name: Optional[str] = None
122
122
  self._flask_app_cache: Optional[Flask] = None
123
123
  self._limiter_enabled = False
@@ -68,8 +68,8 @@ class SwaggerSchema(SwaggerModel):
68
68
  if '$ref' in json.dumps(json_schema):
69
69
  while '$ref' in json.dumps(json_schema):
70
70
  json_schema = replace_value_in_dict(json_schema.copy(), json_schema.copy())
71
- if 'definitions' in json_schema:
72
- del json_schema['definitions']
71
+ if '$defs' in json_schema:
72
+ del json_schema['$defs']
73
73
  self.type = json_schema['type']
74
74
  if json_schema['type'] == 'object':
75
75
  self.properties = json_schema.get('properties', dict())
@@ -508,17 +508,29 @@ class SwaggerPath(SwaggerModel):
508
508
  swagger_request_type.add_swagger_model(parameters)
509
509
  query_schema = query_model.model_json_schema()
510
510
  query_required_fields = set(query_schema.get('required')) if query_schema.get('required') is not None else set()
511
- query_definitions = query_schema.get('definitions')
511
+ query_definitions = query_schema.get('$defs')
512
512
  for parameter_name, parameter_spec in query_schema.get('properties').items():
513
- if parameter_spec.get('type') is not None:
514
- parameter_models.add(SwaggerQueryParameter(
515
- input_type=parameter_spec.get('type'),
516
- input_format=parameter_spec.get('format'),
517
- default_value=parameter_spec.get('default'),
518
- name=parameter_name,
519
- description=parameter_spec.get('description'),
520
- required=parameter_name in query_required_fields,
521
- ))
513
+ if 'anyOf' in parameter_spec.keys():
514
+ if parameter_spec.get('anyOf')[0].get('$ref') is not None:
515
+ definition = query_definitions.get(parameter_spec.get('$ref', '').split('/')[-1], {})
516
+ parameter_models.add(SwaggerQueryParameter(
517
+ input_type=definition.get('type', 'string'),
518
+ input_format=definition.get('format'),
519
+ default_value=definition.get('default'),
520
+ name=parameter_name,
521
+ description=definition.get('description'),
522
+ enum=definition.get('enum'),
523
+ required=parameter_name in query_required_fields,
524
+ ))
525
+ if parameter_spec.get('anyOf')[0].get('type') is not None:
526
+ parameter_models.add(SwaggerQueryParameter(
527
+ input_type=parameter_spec.get('anyOf')[0].get('type'),
528
+ input_format=parameter_spec.get('format'),
529
+ default_value=parameter_spec.get('default'),
530
+ name=parameter_name,
531
+ description=parameter_spec.get('description'),
532
+ required=parameter_name in query_required_fields,
533
+ ))
522
534
  elif parameter_spec.get('$ref') is not None:
523
535
  definition = query_definitions.get(parameter_spec.get('$ref', '').split('/')[-1], {})
524
536
  parameter_models.add(SwaggerQueryParameter(
@@ -530,6 +542,15 @@ class SwaggerPath(SwaggerModel):
530
542
  enum=definition.get('enum'),
531
543
  required=parameter_name in query_required_fields,
532
544
  ))
545
+ elif parameter_spec.get('type') is not None:
546
+ parameter_models.add(SwaggerQueryParameter(
547
+ input_type=parameter_spec.get('type') or parameter_spec.get('anyOf')[0].get('type'),
548
+ input_format=parameter_spec.get('format'),
549
+ default_value=parameter_spec.get('default'),
550
+ name=parameter_name,
551
+ description=parameter_spec.get('description'),
552
+ required=parameter_name in query_required_fields,
553
+ ))
533
554
  if isinstance(parameters, SwaggerModel):
534
555
  parameters.add_swagger_models(parameter_models)
535
556
  elif isinstance(parameters, list):
@@ -5,7 +5,7 @@ def replace_value_in_dict(item: Union[List, Dict], original_schema): # type: ig
5
5
  if isinstance(item, list):
6
6
  return [replace_value_in_dict(i, original_schema) for i in item]
7
7
  elif isinstance(item, dict):
8
- if list(item.keys()) == ['$ref']:
8
+ if '$ref' in list(item.keys()):
9
9
  definitions = item['$ref'][2:].split('/')
10
10
  res = original_schema.copy()
11
11
  for definition in definitions:
@@ -2,7 +2,7 @@ import dataclasses
2
2
  import decimal
3
3
  import json
4
4
  from base64 import b64encode
5
- from datetime import date, datetime
5
+ from datetime import date, datetime, time
6
6
  from enum import Enum
7
7
  from json import JSONEncoder
8
8
  from typing import Dict, Any, Union, List, Optional, TYPE_CHECKING
@@ -66,6 +66,8 @@ class CustomJSONEncoder(JSONEncoder):
66
66
  return str(obj.isoformat())
67
67
  if isinstance(obj, date):
68
68
  return str(obj.isoformat())
69
+ if isinstance(obj, time):
70
+ return str(obj.isoformat())
69
71
  if isinstance(obj, UUID):
70
72
  return str(obj)
71
73
  if isinstance(obj, Enum):
@@ -2,6 +2,8 @@ from dataclasses import dataclass
2
2
  from types import NoneType
3
3
  from typing import NamedTuple, Type, Tuple, _GenericAlias, _UnionGenericAlias, Any, Union, Optional, TypeVar, Generic, Callable # type: ignore
4
4
 
5
+ from ul_api_utils.api_resource.api_response import RootJsonApiResponsePayload
6
+
5
7
 
6
8
  class TypingType(NamedTuple):
7
9
  value: Type[Any]
@@ -47,6 +49,7 @@ def unwrap_typing(t: Type[Any]) -> Union[TypingGeneric, TypingUnion, TypingType,
47
49
 
48
50
  T = TypeVar('T')
49
51
  TVal = TypeVar('TVal')
52
+ TValRoot = TypeVar('TValRoot', bound=RootJsonApiResponsePayload[Any])
50
53
 
51
54
 
52
55
  def default_constructor(value_type: Type[TVal], data: Any) -> TVal:
@@ -72,7 +75,7 @@ class UnwrappedOptionalObjOrListOfObj(Generic[TVal]):
72
75
  return constructor(self.value_type, payload) # type: ignore
73
76
 
74
77
  @staticmethod
75
- def parse(t: Type[Any], type_constraint: Optional[Type[TVal]]) -> 'Optional[UnwrappedOptionalObjOrListOfObj[TVal]]':
78
+ def parse(t: Type[Any], type_constraint: Optional[Type[TVal | TValRoot]]) -> 'Optional[UnwrappedOptionalObjOrListOfObj[TVal]]':
76
79
  unwrapped_t = unwrap_typing(t)
77
80
 
78
81
  many = False
@@ -1,32 +1,32 @@
1
- # import pytest
2
- #
3
- # from pydantic import BaseModel
4
- # from typing import Union, List
5
- # from ul_api_utils.validators.custom_fields import QueryParamsSeparatedList
6
- #
7
- #
8
- # class ModelStr(BaseModel):
9
- # param: QueryParamsSeparatedList[str]
10
- #
11
- #
12
- # class ModelInt(BaseModel):
13
- # param: QueryParamsSeparatedList[int]
14
- #
15
- #
16
- # @pytest.mark.parametrize(
17
- # "model, input_data, expected_output",
18
- # [
19
- # pytest.param(ModelStr, "first_array_element,second,third,this", ["first_array_element", "second", "third", "this"]),
20
- # pytest.param(ModelStr, ["first_array_element,second,third,this"], ["first_array_element", "second", "third", "this"]),
21
- # pytest.param(ModelInt, ["1,2,3,4,5"], [1, 2, 3, 4, 5]),
22
- # pytest.param(ModelStr, 'first_array_element,"second,third",this', ["first_array_element", "second,third", "this"]),
23
- # pytest.param(ModelStr, ['first_array_element,"second,third",this'], ["first_array_element", "second,third", "this"]),
24
- # pytest.param(ModelStr, '"first_array_element,second,third",this, "1,2"', ["first_array_element,second,third", "this", "1,2"]),
25
- # ],
26
- # )
27
- # def test__query_params_separated_list(
28
- # model: Union[ModelStr, ModelInt], input_data: Union[List[str], str], expected_output: List[Union[str, int]],
29
- # ) -> None:
30
- # instance = model(param=input_data) # type: ignore
31
- # assert isinstance(instance.param, list)
32
- # assert instance.param == expected_output
1
+ import pytest
2
+
3
+ from pydantic import BaseModel
4
+ from typing import Union, List
5
+ from ul_api_utils.validators.custom_fields import QueryParamsSeparatedList
6
+
7
+
8
+ class ModelStr(BaseModel):
9
+ param: QueryParamsSeparatedList[str]
10
+
11
+
12
+ class ModelInt(BaseModel):
13
+ param: QueryParamsSeparatedList[int]
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "model, input_data, expected_output",
18
+ [
19
+ pytest.param(ModelStr, "first_array_element,second,third,this", ["first_array_element", "second", "third", "this"]),
20
+ pytest.param(ModelStr, ["first_array_element,second,third,this"], ["first_array_element", "second", "third", "this"]),
21
+ pytest.param(ModelInt, ["1,2,3,4,5"], [1, 2, 3, 4, 5]),
22
+ pytest.param(ModelStr, 'first_array_element,"second,third",this', ["first_array_element", "second,third", "this"]),
23
+ pytest.param(ModelStr, ['first_array_element,"second,third",this'], ["first_array_element", "second,third", "this"]),
24
+ pytest.param(ModelStr, '"first_array_element,second,third",this, "1,2"', ["first_array_element,second,third", "this", "1,2"]),
25
+ ],
26
+ )
27
+ def test__query_params_separated_list(
28
+ model: Union[ModelStr, ModelInt], input_data: Union[List[str], str], expected_output: List[Union[str, int]],
29
+ ) -> None:
30
+ instance = model(param=input_data) # type: ignore
31
+ assert isinstance(instance.param, list)
32
+ assert instance.param == expected_output
@@ -1,10 +1,8 @@
1
1
  import csv
2
- from typing import TypeVar, Generic, List, Union, Generator, Callable, Annotated, Any
2
+ from typing import TypeVar, Generic, List, Union, Annotated, Any, get_args
3
3
  from uuid import UUID
4
4
 
5
- from pydantic import Field, StringConstraints, TypeAdapter
6
- from pydantic_core import ValidationError, InitErrorDetails
7
- from pydantic_core.core_schema import ValidationInfo
5
+ from pydantic import Field, StringConstraints, BeforeValidator
8
6
 
9
7
  from ul_api_utils.const import CRON_EXPRESSION_VALIDATION_REGEX, MIN_UTC_OFFSET_SECONDS, MAX_UTC_OFFSET_SECONDS
10
8
 
@@ -29,56 +27,34 @@ PgTypePositiveInt64Annotation = Annotated[int, Field(ge=0, le=922337203685477580
29
27
  QueryParamsSeparatedListValueType = TypeVar('QueryParamsSeparatedListValueType')
30
28
 
31
29
 
32
- class QueryParamsSeparatedList(Generic[QueryParamsSeparatedListValueType]):
33
- """
34
- Supports cases when query parameters are being sent as a string, but you have to assume
35
- that it is a list.
36
-
37
- F.E. Query string is ?foo=1,2
30
+ def validate_query_params(value: Union[str, List[str]], type_: type) -> List[QueryParamsSeparatedListValueType]:
31
+ def process_item(item: str) -> QueryParamsSeparatedListValueType:
32
+ return type_(item.strip())
38
33
 
39
- Note:
40
- Sent as a string, but interpreted as List.
41
- """
42
- _contains_type: Any = None
34
+ if isinstance(value, list):
35
+ result: list[QueryParamsSeparatedListValueType] = []
36
+ for item in value:
37
+ if isinstance(item, str):
38
+ reader = csv.reader([item], skipinitialspace=True)
39
+ result.extend(process_item(sub_item) for row in reader for sub_item in row)
40
+ else:
41
+ raise ValueError("List items must be strings")
42
+ elif isinstance(value, str):
43
+ reader = csv.reader([value], skipinitialspace=True)
44
+ result = [process_item(item) for row in reader for item in row]
45
+ else:
46
+ raise ValueError("Value must be a string or a list of strings")
43
47
 
44
- @classmethod
45
- def __class_getitem__(cls, item: Any) -> QueryParamsSeparatedListValueType:
46
- new_cls = super().__class_getitem__(item) # type: ignore
47
- new_cls._contains_type = item
48
- return new_cls
48
+ return result
49
49
 
50
- @classmethod
51
- def __get_validators__(cls) -> Generator[Callable[[Union[List[str], str], ValidationInfo], List[QueryParamsSeparatedListValueType]], None, None]:
52
- yield cls.validate
53
50
 
51
+ class QueryParamsSeparatedList(Generic[QueryParamsSeparatedListValueType]):
54
52
  @classmethod
55
- def validate(cls, query_param: Union[List[str], str], info: ValidationInfo) -> List[QueryParamsSeparatedListValueType]:
56
- """
57
- Validate and convert the query parameter string into a list of the specified type.
58
- """
59
- if cls._contains_type is None:
60
- raise TypeError("QueryParamsSeparatedList must be parameterized with a type, e.g., QueryParamsSeparatedList[int]")
61
-
62
- adapter = TypeAdapter(cls._contains_type)
63
-
64
- if not isinstance(query_param, list):
65
- query_param = [query_param]
66
-
67
- reader = csv.reader(query_param, skipinitialspace=True)
68
- splitted = next(reader)
69
-
70
- validated_items = []
71
- errors: List[InitErrorDetails] = []
72
-
73
- for idx, value in enumerate(splitted):
74
- try:
75
- validated_items.append(adapter.validate_python(value))
76
- except ValidationError as e:
77
- for error in e.errors(include_url=False):
78
- error['loc'] = ('param', idx)
79
- errors.append(error) # type: ignore
80
-
81
- if errors:
82
- raise ValidationError.from_exception_data("List validation error", errors)
83
-
84
- return validated_items
53
+ def __get_pydantic_core_schema__(cls, source_type: type, handler: Any) -> Any:
54
+ inner_type = get_args(source_type)[0]
55
+ return handler(
56
+ Annotated[
57
+ List[inner_type], # type: ignore
58
+ BeforeValidator(lambda x: validate_query_params(x, inner_type))
59
+ ]
60
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ul-api-utils
3
- Version: 9.0.0a6
3
+ Version: 9.0.0a7
4
4
  Summary: Python api utils
5
5
  Author: Unic-lab
6
6
  Author-email:
@@ -26,12 +26,12 @@ ul_api_utils/sentry.py,sha256=UH_SwZCAoKH-Nw5B9CVQMoF-b1BJOp-ZTzwqUZ3Oq84,1801
26
26
  ul_api_utils/access/__init__.py,sha256=NUyRNvCVwfePrfdn5ATFVfHeSO3iq4-Syeup4IAZGzs,4526
27
27
  ul_api_utils/api_resource/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  ul_api_utils/api_resource/api_request.py,sha256=l3UNeENPq5pr6ICEF5iPKHT6Hy0bgxjGoKzhKmMyJVk,3486
29
- ul_api_utils/api_resource/api_resource.py,sha256=WMGYtBKoobpRJVNLLWdbuVpq2IcIAI9kK5_Hgd4TqqM,17575
29
+ ul_api_utils/api_resource/api_resource.py,sha256=dc-4eLsj_-2ylcMWp4DBPO0S-JhzZ_IOJtkZVIkBFlo,17649
30
30
  ul_api_utils/api_resource/api_resource_config.py,sha256=l9OYJy75UZLshOkEQDO5jlhXeb5H4HDPu-nLOjuoexw,769
31
31
  ul_api_utils/api_resource/api_resource_error_handling.py,sha256=E0SWpjFSIP-4SumbgzrHtFuFiGe9q38WsvLROt0YcPE,1168
32
- ul_api_utils/api_resource/api_resource_fn_typing.py,sha256=n5bux_azlV8aRtRQdoIP553tXWHCi7P-brKUAgj502E,18970
32
+ ul_api_utils/api_resource/api_resource_fn_typing.py,sha256=gyWEnYuepvaVW4O6ljpoPA5S4i9EeKsYBaAN2mHXDcw,18963
33
33
  ul_api_utils/api_resource/api_resource_type.py,sha256=mgjSQI3swGpgpLI6y35LYtFrdN-kXyV5cQorwGW7h6g,462
34
- ul_api_utils/api_resource/api_response.py,sha256=OnIQepPuJoTm57SGmwTbdosKSLzSZnyEquZp9mjO6vE,9804
34
+ ul_api_utils/api_resource/api_response.py,sha256=kGoYJHa51_vDlCG492AdEqMMmLtdRJIoym5Nwp3MqmY,9773
35
35
  ul_api_utils/api_resource/api_response_db.py,sha256=ucY6ANPlHZml7JAbvq-PL85z0bvERTjEJKvz-REPyok,888
36
36
  ul_api_utils/api_resource/api_response_payload_alias.py,sha256=FoD0LhQGZ2T8A5-VKRX5ADyzSgm7_dd3qxU2BgCVXkA,587
37
37
  ul_api_utils/api_resource/db_types.py,sha256=NxHBYetUogWZow7Vhd3e00Y3L62-dxjwRzJlXywYlV4,439
@@ -50,22 +50,22 @@ ul_api_utils/commands/start/wsgi.py,sha256=ZBFx66XP8iNliK3vkB6yWRkCq2-ItgieweBmW
50
50
  ul_api_utils/conf/ul-debugger-main.js,sha256=XgzaH1AWAG4_PW9mRaoJGJOKeEA5T61j7yIouZjU65s,999747
51
51
  ul_api_utils/conf/ul-debugger-ui.js,sha256=bNwv6ntu8RjCrH33H5eTUtFXdBoMrgFt3P87ujbBmRU,2043
52
52
  ul_api_utils/debug/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- ul_api_utils/debug/debugger.py,sha256=eBsYJjBjvUT2yfr8YuJSNSOETVlbz8jAvTPvIKTUvoY,3716
53
+ ul_api_utils/debug/debugger.py,sha256=dXl7xTMKEvYNKOmo6EG2CK69KrMNGnq7s25yDRttev8,3740
54
54
  ul_api_utils/debug/malloc.py,sha256=OvESxpn8sQMyAb64DxnYUAofRZdnJ1I199IUBWiIoa4,3274
55
- ul_api_utils/debug/stat.py,sha256=LNWhp1tGtLV8u-qmfa3Du_eFaedTB166w7gOhCzdzzQ,14480
55
+ ul_api_utils/debug/stat.py,sha256=Txn-NCeF_edeCuFb7h8o_tI2boeQm92fz9o8M7iifg8,14066
56
56
  ul_api_utils/encrypt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  ul_api_utils/encrypt/encrypt_decrypt_abstract.py,sha256=V2lFKoBxUKO5AYvHWLYv-rTvYXiGDPj3nOkU1noebDI,334
58
58
  ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py,sha256=Gj-X_CoYY2PPrczTcG9Ho_dgordsh9jKB_cVnVEE3XU,2356
59
59
  ul_api_utils/internal_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- ul_api_utils/internal_api/internal_api.py,sha256=0ye53z06naptIN2P3TKDc6fSGVrxyEZNelnED39hr2U,13959
60
+ ul_api_utils/internal_api/internal_api.py,sha256=45fHQdNXLAhmw1w-cAQJglJ8b8ZDL8_Ibm_9sG4zeO4,13967
61
61
  ul_api_utils/internal_api/internal_api_check_context.py,sha256=OEDkI06wArjBNSjRSzARvEIPDhTzmfvYThgpvFJgUZU,1490
62
62
  ul_api_utils/internal_api/internal_api_error.py,sha256=sdm3V2VLSfFVBmxaeo2Wy2wkhmxWTXGsCCR-u08ChMg,471
63
- ul_api_utils/internal_api/internal_api_response.py,sha256=rNAqY82ezupcRSnWY1YO2T5QhwfOFrak0dp23shgUxY,11583
63
+ ul_api_utils/internal_api/internal_api_response.py,sha256=TyS7iLbvTFb0oAcElK3fUAB5jNGQDtiJUrESn5BTTHM,11917
64
64
  ul_api_utils/internal_api/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  ul_api_utils/internal_api/__tests__/internal_api.py,sha256=X2iopeso6vryszeeA__lcqXQVtz3Nwt3ngH7M4OuN1U,1116
66
66
  ul_api_utils/internal_api/__tests__/internal_api_content_type.py,sha256=mfiYPkzKtfZKFpi4RSnWAoCd6mRijr6sFsa2TF-s5t8,749
67
67
  ul_api_utils/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- ul_api_utils/modules/api_sdk.py,sha256=BOQdHXzajmRW2tnGym-4PjLi_ypTn54KDJt3DaaUcss,26430
68
+ ul_api_utils/modules/api_sdk.py,sha256=w2STpA6znKrrsD3iSTs07vfV_K2w_gSEWNGpz_Q9bZc,26438
69
69
  ul_api_utils/modules/api_sdk_config.py,sha256=ZUR48tIJeFlPJTSjyXzKfXaCKPtfqeaA0mlLX42SSFY,2137
70
70
  ul_api_utils/modules/api_sdk_jwt.py,sha256=2XRfb0LxHUnldSL67S60v1uyoDpVPNaq4zofUtkeg88,15112
71
71
  ul_api_utils/modules/intermediate_state.py,sha256=7ZZ3Sypbb8LaSfrVhaXaWRDnj8oyy26NUbmFK7vr-y4,1270
@@ -111,11 +111,11 @@ ul_api_utils/utils/deprecated.py,sha256=xR3ELgoDj7vJEY4CAYeEhdbtSJTfkukbjxcULtpM
111
111
  ul_api_utils/utils/flags.py,sha256=AYN5nKWp4-uu6PSlPptL7ZiLqr3Pu-x5dffF6SBsqfg,957
112
112
  ul_api_utils/utils/imports.py,sha256=i8PhoD0c_jnWTeXt_VxW_FihynwXSL_dHRT7jQiFyXE,376
113
113
  ul_api_utils/utils/instance_checks.py,sha256=9punTfY5uabuJhmSGLfTgBqRderoFysCXBSI8rvbPco,467
114
- ul_api_utils/utils/json_encoder.py,sha256=tLcO8da4iytj9sCzuD8hP7XO3qPujBmGbuhAOOX9vsI,4711
114
+ ul_api_utils/utils/json_encoder.py,sha256=k8LlEvac3nXpk30S8t5YJir9HipcRHPVVMbYqnDYWV4,4791
115
115
  ul_api_utils/utils/load_modules.py,sha256=_CPmQuB6o_33FE6zFl_GyO5xS5gmjfNffB6k-cglKAA,685
116
116
  ul_api_utils/utils/token_check.py,sha256=-Quuh8gOs9fNE1shYhdiMpQedafsLN7MB2ilSxG_F8E,489
117
117
  ul_api_utils/utils/token_check_through_request.py,sha256=OyyObu6Btk9br7auIYvWcMULhNznNSD5T0mWOwZX7Uk,663
118
- ul_api_utils/utils/unwrap_typing.py,sha256=19jyhkRcb89v1gzR62YDsMO9zxR2PU5bjYZm1is8Aoo,4189
118
+ ul_api_utils/utils/unwrap_typing.py,sha256=uObDLxYcRni6ieN9FuvWTa350HjSUIpgTb7FrIUzsVI,4349
119
119
  ul_api_utils/utils/uuid_converter.py,sha256=OZMuySkoALrQQOe312_BHVWN20Sz5frKuH9KYziAGsU,565
120
120
  ul_api_utils/utils/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
121
  ul_api_utils/utils/__tests__/api_path_version.py,sha256=n7VFVdwFqP_FS6PE3OUCS68oU3tG78xM4HxrKShLhNw,898
@@ -126,12 +126,12 @@ ul_api_utils/utils/flask_swagger_generator/exceptions.py,sha256=yA4IsUyxh5puyoYz
126
126
  ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
127
  ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py,sha256=P52azB25Ncpp6I5j5bq4LLAJHDLSORFVJp9bHoXvpQk,1698
128
128
  ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py,sha256=9Cf1ijk90IDYUDG8kTjK4cxjdxpYjWZz1tKJHPCeDAA,1513
129
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py,sha256=4zkH3P41DwPXsriJISc7aL0IYjKR7yKfoKAzf9arQW0,30837
129
+ ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py,sha256=XxQ0IdC-WRs_qgS0tFcD_yqApdH_gLR--CB-uvB29JA,32355
130
130
  ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py,sha256=A14IRG-e2KL2SlFbHep2FH0uMRIHPhfd7KLkYdtWrfA,1312
131
131
  ul_api_utils/utils/flask_swagger_generator/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
132
  ul_api_utils/utils/flask_swagger_generator/utils/input_type.py,sha256=Ynp3zI5q1F0Tl_eTdNbWoCxRKPwBCJkwJOoeHE2pTRE,2533
133
133
  ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py,sha256=bTE_kDUPNYL7Qr1AV_h4pXEWiLeiVYVFBxAWMkLYzPU,1557
134
- ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py,sha256=B8FyQDRGLL8wQ8e7maq3PfHLffNSGSMu-EOqX4YKrUE,744
134
+ ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py,sha256=xGF78881vi8xAWbJ9eaqBzsuMFHSJGZNYlm6bmC-5jk,742
135
135
  ul_api_utils/utils/flask_swagger_generator/utils/request_type.py,sha256=fx4ltfODfYmXx3i31BTZGAtwzrMqYbk48VQavQirFa8,1504
136
136
  ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py,sha256=NqFRHjSWZgg6fNYE-CG7vwLa57ie9WjooB2YfVTs4UM,294
137
137
  ul_api_utils/utils/flask_swagger_generator/utils/security_type.py,sha256=AcWOQZBbUPXZyyTbmGFRt5p4Jx1uwETuxNRtXXht16I,1148
@@ -143,14 +143,14 @@ ul_api_utils/utils/memory_db/errors.py,sha256=Bw1O1Y_WTCeCRNgb9iwrGT1fst6iHORhrN
143
143
  ul_api_utils/utils/memory_db/repository.py,sha256=0AI0-VA4Md54BrAuZLX0Lg7rxyyGv54aWiiNUPycGSA,3760
144
144
  ul_api_utils/utils/memory_db/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
145
  ul_api_utils/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
- ul_api_utils/validators/custom_fields.py,sha256=LKmsn2R66BRPaDiZDqXmC7wCKxDK8OTD1Jk8fFj0_So,3778
146
+ ul_api_utils/validators/custom_fields.py,sha256=6pNvwHMhup9pqGOvqp57UsPAfmgvVyLujBKooBY-C1I,2998
147
147
  ul_api_utils/validators/validate_empty_object.py,sha256=3Ck_iwyJE_M5e7l6s1i88aqb73zIt06uaLrMG2PAb0A,299
148
148
  ul_api_utils/validators/validate_uuid.py,sha256=EfvlRirv2EW0Z6w3s8E8rUa9GaI8qXZkBWhnPs8NFrA,257
149
149
  ul_api_utils/validators/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
- ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=20gLlnm1Ithsbbz3NIUXVAd92lW6YwVRSg_nETZhfaI,1442
151
- ul_api_utils-9.0.0a6.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
152
- ul_api_utils-9.0.0a6.dist-info/METADATA,sha256=7yub8rFH2_lYlZ2nhyfyoWkgPiyPWhFliFIytBYLuoI,14714
153
- ul_api_utils-9.0.0a6.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
154
- ul_api_utils-9.0.0a6.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
155
- ul_api_utils-9.0.0a6.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
156
- ul_api_utils-9.0.0a6.dist-info/RECORD,,
150
+ ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=omXI_PPefDfCehEVJxEevep8phY6aySjLnpW_usT85U,1385
151
+ ul_api_utils-9.0.0a7.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
152
+ ul_api_utils-9.0.0a7.dist-info/METADATA,sha256=R2-BjpDjq_LDuDxY0K1b_ZCPFKXeFgvwBm_i7tGRB08,14714
153
+ ul_api_utils-9.0.0a7.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
154
+ ul_api_utils-9.0.0a7.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
155
+ ul_api_utils-9.0.0a7.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
156
+ ul_api_utils-9.0.0a7.dist-info/RECORD,,