Skip to content

Commit 899b2af

Browse files
shuvyAshuvypermit
andauthored
Shuvy/per 12881 add list, create, get and delete invite user to python sdk (#120)
* Update code-generator * add list, create, get, delete api in user+invite * Fix pre commit * Fix test and add setup for test * Fix model_dump --------- Co-authored-by: Shuvy <[email protected]>
1 parent 3cc9ecf commit 899b2af

File tree

5 files changed

+1675
-221
lines changed

5 files changed

+1675
-221
lines changed

permit/api/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from loguru import logger
66

77
from ..utils.pydantic_version import PYDANTIC_VERSION
8+
from .encoders import jsonable_encoder
89

910
if PYDANTIC_VERSION < (2, 0):
1011
from pydantic import BaseModel, Extra, Field, parse_obj_as
@@ -62,7 +63,7 @@ def _prepare_json(self, json: Optional[Union[TData, dict, list]] = None) -> Opti
6263
if isinstance(json, list):
6364
return [self._prepare_json(item) for item in json]
6465

65-
return json.dict(exclude_unset=True, exclude_none=True)
66+
return jsonable_encoder(json, exclude_unset=True, exclude_none=True)
6667

6768
@handle_client_error
6869
async def get(self, url, model: Type[TModel], **kwargs) -> TModel:

permit/api/encoders.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# copied from fastapi.encoders
2+
import dataclasses
3+
import datetime
4+
from collections import defaultdict, deque
5+
from decimal import Decimal
6+
from enum import Enum
7+
from ipaddress import (
8+
IPv4Address,
9+
IPv4Interface,
10+
IPv4Network,
11+
IPv6Address,
12+
IPv6Interface,
13+
IPv6Network,
14+
)
15+
from pathlib import Path, PurePath
16+
from re import Pattern
17+
from types import GeneratorType
18+
from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Type, Union
19+
from uuid import UUID
20+
21+
from permit import PYDANTIC_VERSION
22+
23+
if PYDANTIC_VERSION < (2, 0):
24+
from pydantic import BaseModel
25+
from pydantic.color import Color
26+
from pydantic.networks import AnyUrl, NameEmail
27+
from pydantic.types import SecretBytes, SecretStr
28+
29+
def _model_dump(model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any) -> Any: # noqa: ARG001
30+
return model.dict(**kwargs)
31+
else:
32+
from pydantic.v1 import BaseModel # type: ignore[assignment]
33+
from pydantic.v1.color import Color # type: ignore[assignment]
34+
from pydantic.v1.networks import AnyUrl, NameEmail # type: ignore[assignment]
35+
from pydantic.v1.types import SecretBytes, SecretStr # type: ignore[assignment]
36+
37+
def _model_dump(model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any) -> Any: # noqa: ARG001
38+
return model.dict(**kwargs)
39+
40+
41+
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
42+
return o.isoformat()
43+
44+
45+
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
46+
"""
47+
Encodes a Decimal as int of there's no exponent, otherwise float
48+
49+
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
50+
where a integer (but not int typed) is used. Encoding this as a float
51+
results in failed round-tripping between encode and parse.
52+
Our Id type is a prime example of this.
53+
54+
>>> decimal_encoder(Decimal("1.0"))
55+
1.0
56+
57+
>>> decimal_encoder(Decimal("1"))
58+
1
59+
"""
60+
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
61+
return int(dec_value)
62+
else:
63+
return float(dec_value)
64+
65+
66+
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
67+
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
68+
bytes: lambda o: o.decode(),
69+
Color: str,
70+
datetime.date: isoformat,
71+
datetime.datetime: isoformat,
72+
datetime.time: isoformat,
73+
datetime.timedelta: lambda td: td.total_seconds(),
74+
Decimal: decimal_encoder,
75+
Enum: lambda o: o.value,
76+
frozenset: list,
77+
deque: list,
78+
GeneratorType: list,
79+
IPv4Address: str,
80+
IPv4Interface: str,
81+
IPv4Network: str,
82+
IPv6Address: str,
83+
IPv6Interface: str,
84+
IPv6Network: str,
85+
NameEmail: str,
86+
Path: str,
87+
Pattern: lambda o: o.pattern,
88+
SecretBytes: str,
89+
SecretStr: str,
90+
set: list,
91+
UUID: str,
92+
AnyUrl: str,
93+
}
94+
95+
96+
def generate_encoders_by_class_tuples(
97+
type_encoder_map: Dict[Any, Callable[[Any], Any]],
98+
) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]:
99+
encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple)
100+
for type_, encoder in type_encoder_map.items():
101+
encoders_by_class_tuples[encoder] += (type_,)
102+
return encoders_by_class_tuples
103+
104+
105+
encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
106+
107+
108+
def jsonable_encoder(
109+
obj: Any,
110+
*,
111+
include: Optional[IncEx] = None,
112+
exclude: Optional[IncEx] = None,
113+
by_alias: bool = True,
114+
exclude_unset: bool = False,
115+
exclude_defaults: bool = False,
116+
exclude_none: bool = False,
117+
custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None,
118+
sqlalchemy_safe: bool = True,
119+
) -> Any:
120+
"""
121+
Convert any object to something that can be encoded in JSON.
122+
123+
This is used internally by FastAPI to make sure anything you return can be
124+
encoded as JSON before it is sent to the client.
125+
126+
You can also use it yourself, for example to convert objects before saving them
127+
in a database that supports only JSON.
128+
129+
Read more about it in the
130+
[FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/).
131+
"""
132+
custom_encoder = custom_encoder or {}
133+
if custom_encoder:
134+
if type(obj) in custom_encoder:
135+
return custom_encoder[type(obj)](obj)
136+
else:
137+
for encoder_type, encoder_instance in custom_encoder.items():
138+
if isinstance(obj, encoder_type):
139+
return encoder_instance(obj)
140+
if include is not None and not isinstance(include, (set, dict)):
141+
include = set(include) # type: ignore[unreachable]
142+
if exclude is not None and not isinstance(exclude, (set, dict)):
143+
exclude = set(exclude) # type: ignore[unreachable]
144+
if isinstance(obj, BaseModel):
145+
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
146+
if custom_encoder:
147+
encoders.update(custom_encoder)
148+
149+
obj_dict = _model_dump(
150+
obj,
151+
mode="json",
152+
include=include,
153+
exclude=exclude,
154+
by_alias=by_alias,
155+
exclude_unset=exclude_unset,
156+
exclude_none=exclude_none,
157+
exclude_defaults=exclude_defaults,
158+
)
159+
if "__root__" in obj_dict:
160+
obj_dict = obj_dict["__root__"]
161+
return jsonable_encoder(
162+
obj_dict,
163+
exclude_none=exclude_none,
164+
exclude_defaults=exclude_defaults,
165+
# TODO: remove when deprecating Pydantic v1
166+
custom_encoder=encoders,
167+
sqlalchemy_safe=sqlalchemy_safe,
168+
)
169+
if dataclasses.is_dataclass(obj):
170+
obj_dict = dataclasses.asdict(obj) # type: ignore[call-overload]
171+
return jsonable_encoder(
172+
obj_dict,
173+
include=include,
174+
exclude=exclude,
175+
by_alias=by_alias,
176+
exclude_unset=exclude_unset,
177+
exclude_defaults=exclude_defaults,
178+
exclude_none=exclude_none,
179+
custom_encoder=custom_encoder,
180+
sqlalchemy_safe=sqlalchemy_safe,
181+
)
182+
if isinstance(obj, Enum):
183+
return obj.value
184+
if isinstance(obj, PurePath):
185+
return str(obj)
186+
if isinstance(obj, (str, int, float, type(None))):
187+
return obj
188+
if isinstance(obj, dict):
189+
encoded_dict = {}
190+
allowed_keys = set(obj.keys())
191+
if include is not None:
192+
allowed_keys &= set(include)
193+
if exclude is not None:
194+
allowed_keys -= set(exclude)
195+
for key, value in obj.items():
196+
if (
197+
(not sqlalchemy_safe or (not isinstance(key, str)) or (not key.startswith("_sa")))
198+
and (value is not None or not exclude_none)
199+
and key in allowed_keys
200+
):
201+
encoded_key = jsonable_encoder(
202+
key,
203+
by_alias=by_alias,
204+
exclude_unset=exclude_unset,
205+
exclude_none=exclude_none,
206+
custom_encoder=custom_encoder,
207+
sqlalchemy_safe=sqlalchemy_safe,
208+
)
209+
encoded_value = jsonable_encoder(
210+
value,
211+
by_alias=by_alias,
212+
exclude_unset=exclude_unset,
213+
exclude_none=exclude_none,
214+
custom_encoder=custom_encoder,
215+
sqlalchemy_safe=sqlalchemy_safe,
216+
)
217+
encoded_dict[encoded_key] = encoded_value
218+
return encoded_dict
219+
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
220+
encoded_list = []
221+
for item in obj:
222+
encoded_list.append(
223+
jsonable_encoder(
224+
item,
225+
include=include,
226+
exclude=exclude,
227+
by_alias=by_alias,
228+
exclude_unset=exclude_unset,
229+
exclude_defaults=exclude_defaults,
230+
exclude_none=exclude_none,
231+
custom_encoder=custom_encoder,
232+
sqlalchemy_safe=sqlalchemy_safe,
233+
)
234+
)
235+
return encoded_list
236+
237+
if type(obj) in ENCODERS_BY_TYPE:
238+
return ENCODERS_BY_TYPE[type(obj)](obj)
239+
for encoder, classes_tuple in encoders_by_class_tuples.items():
240+
if isinstance(obj, classes_tuple):
241+
return encoder(obj)
242+
243+
try:
244+
data = dict(obj)
245+
except Exception as e: # noqa: BLE001
246+
errors: List[Exception] = []
247+
errors.append(e)
248+
try:
249+
data = vars(obj)
250+
except Exception as e:
251+
errors.append(e)
252+
raise ValueError(errors) from e
253+
return jsonable_encoder(
254+
data,
255+
include=include,
256+
exclude=exclude,
257+
by_alias=by_alias,
258+
exclude_unset=exclude_unset,
259+
exclude_defaults=exclude_defaults,
260+
exclude_none=exclude_none,
261+
custom_encoder=custom_encoder,
262+
sqlalchemy_safe=sqlalchemy_safe,
263+
)

0 commit comments

Comments
 (0)