sqlalchemy-serializer-meta 0.1.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.
@@ -0,0 +1,424 @@
1
+ """
2
+ # Serializer Models
3
+ """
4
+
5
+ from sqlalchemy import Table
6
+ from .serializer import (
7
+ AbstractSerializer,
8
+ Validatable,
9
+ Saveable,
10
+ Updatable,
11
+ Deletable,
12
+ Representable,
13
+ Creatable,
14
+ ToInstance,
15
+ ExtendsPydantic,
16
+ )
17
+ from typing import Self, NoReturn, Optional, Any, Union
18
+ from pydantic import BaseModel, ValidationError
19
+
20
+
21
+ # Serializer Exceptions
22
+ class SerializerError(Exception):
23
+ """
24
+ Serializer Error
25
+
26
+ You must apply excpetion as following otherwise an error will occure
27
+
28
+ ```py
29
+ from playstation import serializer
30
+
31
+ raise serializer.SerializerError(TypeError, "Error Message")
32
+
33
+ # Your code will be catched to form a message like this
34
+ try:
35
+ # Code
36
+ except SerializerError as e:
37
+ message: str = e.args[1] # Exception message
38
+ type: Exception = e.args[0] # Exception Type
39
+ """
40
+
41
+ pass
42
+
43
+
44
+ # Serializer Interface
45
+ class SerializerInterface(
46
+ AbstractSerializer,
47
+ Validatable,
48
+ Saveable,
49
+ Updatable,
50
+ Deletable,
51
+ Representable,
52
+ Creatable,
53
+ ToInstance,
54
+ ExtendsPydantic,
55
+ ):
56
+ pass
57
+
58
+
59
+ # Serializer class
60
+ class Serializer(SerializerInterface):
61
+ @property
62
+ def data(self: Self) -> dict:
63
+ """
64
+ Property to get data
65
+ """
66
+ # Check instance
67
+ if self.instance:
68
+ return self.to_representation(self.instance)
69
+ # Return self._data
70
+ return self.validated_data
71
+
72
+ @property
73
+ def validated_data(self: Self) -> dict:
74
+ """
75
+ Property to get validated data
76
+
77
+ Returns:
78
+ - Python Dictionary of the validated data
79
+ """
80
+ # Check validated data
81
+ if not hasattr(self, "_validated_data"):
82
+ raise Exception("You Should run .is_valid() method first to validate data.")
83
+ # Return validated_data
84
+ return self._validated_data
85
+
86
+ def is_valid(self: Self) -> bool:
87
+ """
88
+ Method to validate data
89
+
90
+ Returns:
91
+ - True incase data validation passes otherwise False
92
+ """
93
+ # Implement validation logic here
94
+ # Grab the data
95
+ data: Optional[dict] = self._data
96
+
97
+ # Check data
98
+ self.__data_checker()
99
+
100
+ # validate data
101
+ if self.validate(data):
102
+ return True
103
+ # Data not valid
104
+ return False
105
+
106
+ def __data_checker(self: Self) -> Optional[NoReturn]:
107
+ """
108
+ Method used to check private attribute _data
109
+ """
110
+ # Grab data
111
+ data: Optional[dict] = self._data
112
+ # Check if it is not None
113
+ if data is None:
114
+ raise ValueError("data: parameter should not be None")
115
+ # Check datatype for self._data
116
+ if not isinstance(data, dict):
117
+ raise TypeError("data: parameter should be of type dictionary")
118
+
119
+ def validate(self: Self, data: dict) -> bool:
120
+ """
121
+ Method used to validate data.
122
+
123
+ Creates:
124
+ self._validated_data is created when using this function
125
+
126
+ Returns:
127
+ - bool True in case of validated otherwise False
128
+ """
129
+ # Implement your validation logic here
130
+ # Check data keys if match the fields of the model or mismatch
131
+ self.__validate_data_fields(data)
132
+ # Get all validate methods
133
+ validate_methods: list[str] = self.__validate_methods()
134
+ # Create validated_data
135
+ self._validated_data: dict = data
136
+ # Get Fields
137
+ fields: set[str] = set(self.fields)
138
+ # Check Validation methods
139
+ if validate_methods:
140
+ for method in validate_methods:
141
+ try:
142
+ # Grab key
143
+ key: str = method.replace("validate_", "")
144
+ # Grab the field
145
+ value: Optional[str] = data.get(key)
146
+ # Check if field is in fields
147
+ if key in fields:
148
+ # Call each validation method
149
+ self._validated_data[key] = getattr(self, method)(value)
150
+ except SerializerError as e:
151
+ # add to error
152
+ self.errors.append(
153
+ {
154
+ "msg": str(e.args[1]),
155
+ "field": key,
156
+ "input": (
157
+ value
158
+ if key != "password"
159
+ else "" if key in self.write_only else ""
160
+ ),
161
+ "type": f"{e.args[0].__name__}",
162
+ }
163
+ )
164
+ except Exception as e:
165
+ self.errors.append(
166
+ {
167
+ "msg": str(e),
168
+ "field": key,
169
+ "input": (
170
+ value
171
+ if key != "password"
172
+ else "" if key in self.write_only else ""
173
+ ),
174
+ "type": f"{type(e).__name__}",
175
+ }
176
+ )
177
+ # Validate using pydantic
178
+ if self.pydantic_model is not None:
179
+ self._validated_data = getattr(self, "validate_pydantic")(
180
+ self.validated_data
181
+ )
182
+
183
+ return not bool(self.errors)
184
+
185
+ def __validate_data_fields(self: Self, data: dict) -> Optional[NoReturn]:
186
+ """
187
+ Method used to validate data fields
188
+ """
189
+ # Grab fields
190
+ fields: set = {
191
+ key for key in self.model.__dict__.keys() if not key.startswith("_")
192
+ }
193
+ # Check if data keys match with fields
194
+ for key in data.keys():
195
+ if key not in fields:
196
+ raise ValueError(f"Invalid field: {key}")
197
+
198
+ def __validate_methods(self: Self) -> list[str]:
199
+ """
200
+ Method to retrieve a list of validation callable methods.
201
+
202
+ Returns:
203
+ - list of (callable) methods
204
+ """
205
+ return [
206
+ method
207
+ for method in dir(self)
208
+ if method.startswith("validate_") # Get all validate methods
209
+ if method.replace("validate_", "")
210
+ in self.fields # Check if validation method matchs name of a field
211
+ if callable(getattr(self, method)) # Check if it is a callable method
212
+ ]
213
+
214
+ def validate_pydantic(self: Self, validated_data: dict) -> Optional[BaseModel]:
215
+ """Validate data using Pydantic."""
216
+ try:
217
+ model: BaseModel = getattr(self, "pydantic_model")
218
+ if not model:
219
+ raise AttributeError("No Pydantic model defined")
220
+ return model(**validated_data).model_dump()
221
+ except ValidationError as e:
222
+ self.errors.extend(e.errors())
223
+ except SerializerError as e:
224
+ # add to error
225
+ self.errors.append(
226
+ {
227
+ "msg": str(
228
+ e.args[1]
229
+ ), # error message SerializerError(<class Exception>, error_message)
230
+ "type": f"{e.args[0].__name__}", # Excpetion class name,
231
+ }
232
+ )
233
+ except AttributeError as e:
234
+ # error
235
+ error: dict = {"msg": str(e), "type": f"{type(e).__name__}"}
236
+ self.errors.append(error)
237
+ except Exception as e:
238
+ # error
239
+ error: dict = {"msg": str(e), "type": f"{type(e).__name__}"}
240
+ self.errors.append(error)
241
+ return None
242
+
243
+ def save(self: Self) -> object:
244
+ """
245
+ Method to create new instance of an object or update an existing object
246
+
247
+ Returns:
248
+ - Instance of the model.
249
+ """
250
+ # Grab the data
251
+ data: Optional[dict] = getattr(self, "validated_data", None)
252
+ # Check data
253
+ if data is None:
254
+ raise ValueError("No validated data found")
255
+ # Check if save method exists
256
+ self.__check_save()
257
+ # Check if object instance exists or not
258
+ # Grab object id
259
+ object_id: Optional[int] = data.get("id", None)
260
+ instance: Optional[None] = None
261
+ # Check if id exists
262
+ if object_id:
263
+ # Check if object instance exists or not
264
+ instance: object = self.model.query.get(object_id)
265
+ if instance:
266
+ # Update instance
267
+ self.instance = self.update(data, instance)
268
+ return self.instance
269
+ # Else perform create new instance method
270
+ self.instance = self.create(data)
271
+ # Return the instance
272
+ return self.instance
273
+
274
+ def __check_save(self: Self) -> Optional[NoReturn]:
275
+ """
276
+ Checker function to check if model object has a method .save()
277
+
278
+ Raises:
279
+ - Error in case the model does not have a method .save()
280
+ """
281
+ if not hasattr(self.model, "save"):
282
+ raise AttributeError("Model object must have a method .save()")
283
+
284
+ def update(self: Self, validated_data: dict, instance: object) -> object:
285
+ """
286
+ Method to update an existing object
287
+ """
288
+ # Implement update logic here
289
+ for key, value in validated_data.items():
290
+ # Check if model has the key and key is not in read_only fields
291
+ if hasattr(self.model, key) and key not in self.read_only:
292
+ setattr(instance, key, value)
293
+ # Save
294
+ instance.save()
295
+ # Return instance
296
+ return instance
297
+
298
+ def delete(self: Self) -> None:
299
+ """
300
+ Method to delete an existing object
301
+ """
302
+ # Implement delete logic here
303
+ instance: object = self.instance
304
+ # Check instance
305
+ if not instance:
306
+ raise ValueError("Instance does not exist")
307
+ # Perform delete function
308
+ if not hasattr(instance, "delete"):
309
+ raise AttributeError("Model object must have a method .delete()")
310
+ # Initiate delete instance
311
+ instance.delete()
312
+
313
+ def create(self, validated_data: dict) -> object:
314
+ """
315
+ Create a new instance of the model.
316
+
317
+ Args:
318
+ - validated_data (dict): Validated data of the parameters of new instance.
319
+
320
+ Returns:
321
+ - Newly created instance of the model.
322
+ """
323
+ # Create the model
324
+ instance = self.model(**validated_data)
325
+ # Create instance
326
+ instance.save()
327
+ # Return the instance
328
+ return instance
329
+
330
+ def to_representation(self, instance: object) -> dict:
331
+ """
332
+ Convert the model instance into a dictionary representation.
333
+ """
334
+ # Implement representation logic here
335
+ fields: list[str] = self.fields
336
+ # Serialize
337
+ serialized_data: dict = {
338
+ field: self.__grab_field(instance, field)
339
+ for field in fields
340
+ if field not in self.write_only
341
+ }
342
+ # Return Serialized data
343
+ return serialized_data
344
+
345
+ def __grab_field(self, instance: object, field: str) -> Any:
346
+ """Helper function to grab field for serialization process."""
347
+ # Grab field value from instance_dict
348
+ value: Any = getattr(instance, field)
349
+ # Grab nested serializer
350
+ # If value is None, check if the field is a relationship
351
+ if self.__check_model(value):
352
+ nested_serialization = getattr(self, field, None)
353
+ return self.__serialize_relationship(nested_serialization, value)
354
+ if isinstance(value, list):
355
+ nested_serialization = getattr(self, field, None)
356
+ return self.__serialize_relationship_list(nested_serialization, value)
357
+ # Return Value
358
+ return value
359
+
360
+ def __check_model(self, instance: object) -> bool:
361
+ """Helper function to check if the model exists."""
362
+ # Check if the model exists
363
+ return hasattr(instance, "__table__") and isinstance(instance.__table__, Table)
364
+
365
+ def __serialize_relationship(
366
+ self,
367
+ nested_serializer: Optional["Serializer"],
368
+ relationship: object,
369
+ ) -> Any:
370
+ """Helper function to serialize a single nested relationship."""
371
+ if nested_serializer and isinstance(nested_serializer, Serializer):
372
+ nested_serializer.instance = relationship
373
+ return nested_serializer.data
374
+ return self.__grab_field(relationship, "id")
375
+
376
+ def __serialize_relationship_list(
377
+ self, nested_serializer: Optional["Serializer"], relationship: list
378
+ ) -> list:
379
+ """Helper function to serialize a list of nested relationships."""
380
+ if nested_serializer and isinstance(nested_serializer, Serializer):
381
+ nested_serializer.instance = relationship
382
+ return nested_serializer.data
383
+ return [
384
+ self.__grab_field(model, "id")
385
+ for model in relationship
386
+ if self.__check_model(model)
387
+ ]
388
+
389
+ def to_instance(self, find_by: str = "id") -> Optional[object]:
390
+ """
391
+ Convert the dictionary representation into a model instance.
392
+ """
393
+ # Implement instance creation logic here
394
+ if self._data:
395
+ instance: object = self.model.query.get(self._data[find_by])
396
+ return instance
397
+ return None
398
+
399
+
400
+ # Model Serializer
401
+ class ModelSerializer(Serializer):
402
+ @property
403
+ def data(self: Self) -> dict:
404
+ """
405
+ Property to get serialized model
406
+ """
407
+ # Get the model
408
+ model: object = self.instance
409
+ # Check model
410
+ if model is None:
411
+
412
+ raise ValueError("Model instance is not provided")
413
+
414
+ # Treat instance as many instances
415
+ if self.many:
416
+ if not isinstance(self.instance, list):
417
+ raise TypeError("Expected a list of instances with many set to True")
418
+ return [self.to_representation(instance) for instance in self.instance]
419
+
420
+ if isinstance(model, list):
421
+ model = model[0]
422
+
423
+ # Call to_representation method
424
+ return self.to_representation(model)
@@ -0,0 +1,199 @@
1
+ """
2
+ # File that contains abstract classes for how serializer should be implemented.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Optional, Iterable, Self
7
+ from pydantic import BaseModel
8
+
9
+
10
+ # Abstract Serializer
11
+ class AbstractSerializer(ABC):
12
+ """
13
+ Abstract class for serializer implementation.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ instance: Optional[object] = None,
19
+ data: Optional[dict] = None,
20
+ many: bool = False,
21
+ ) -> None:
22
+ self.instance: Optional[object] = instance
23
+ self._data: Optional[dict] = data
24
+ self.many: bool = many
25
+ self._errors = []
26
+ self._meta = self.get_meta() # Access Meta Class
27
+ self._fields: Optional[Iterable[str]] = getattr(self._meta, "fields", None)
28
+ self._write_only: Iterable[str] = getattr(self._meta, "write_only", [])
29
+ self._read_only: Iterable[str] = getattr(self._meta, "read_only", {"id"})
30
+
31
+ self.fields = self._fields # Trigger the fields setter
32
+ self.write_only = self._write_only # Trigger the write_only setter
33
+ self.read_only = self._read_only # Trigger the read_only setter
34
+
35
+ def get_meta(self):
36
+ meta = getattr(self, "Meta", None)
37
+ if meta is None:
38
+ raise AttributeError("Meta class is not defined.")
39
+ return meta
40
+
41
+ @property
42
+ def data(self) -> dict:
43
+ """
44
+ Property to get serialized model
45
+ """
46
+ raise NotImplementedError("data property must be implemented by subclasses")
47
+
48
+ @property
49
+ def model(self):
50
+ return getattr(self._meta, "model", None)
51
+
52
+ @property
53
+ def fields(self) -> Optional[Iterable[str]]:
54
+ return self._fields
55
+
56
+ @fields.setter
57
+ def fields(self, serializer_fields: Optional[Iterable[str]]) -> None:
58
+ """
59
+ Setter property method for fields.
60
+
61
+ Args:
62
+ - serializer_fields (Iterable Python Object): Fields for serializer.
63
+ """
64
+ if serializer_fields is None:
65
+ raise ValueError("Fields cannot be None.")
66
+ fields = [key for key in self.model.__dict__.keys() if not key.startswith("_")]
67
+ if not self.__check_fields(serializer_fields):
68
+ fields = [
69
+ key for key in serializer_fields if self.__existing_field(key, fields)
70
+ ]
71
+ self._fields = fields
72
+
73
+ @property
74
+ def write_only(self) -> Iterable[str]:
75
+ return self._write_only
76
+
77
+ @write_only.setter
78
+ def write_only(self, serializer_fields: Optional[Iterable[str]]) -> None:
79
+ """
80
+ Setter property method write_only fields.
81
+
82
+ Args:
83
+ - serializer_fields (Iterable Python Object): Fields for serializer.
84
+ """
85
+ if serializer_fields is None:
86
+ return
87
+ fields = [key for key in self.model.__dict__.keys()]
88
+ if not self.__check_fields(serializer_fields):
89
+ fields = [
90
+ key for key in serializer_fields if self.__existing_field(key, fields)
91
+ ]
92
+ self._write_only = fields
93
+
94
+ @property
95
+ def read_only(self) -> Iterable[str]:
96
+ return self._read_only
97
+
98
+ @read_only.setter
99
+ def read_only(self, serializer_fields: Optional[Iterable[str]]) -> None:
100
+ """
101
+ Setter property method read_only fields.
102
+
103
+ Args:
104
+ - serializer_fields (Iterable Python Object): Fields for serializer.
105
+ """
106
+ if serializer_fields is None:
107
+ return
108
+ fields = [key for key in self.model.__dict__.keys()]
109
+ if not self.__check_fields(serializer_fields):
110
+ fields = [
111
+ key for key in serializer_fields if self.__existing_field(key, fields)
112
+ ]
113
+ self._read_only = fields
114
+
115
+ def __existing_field(self, field: str, fields: list[str]) -> bool:
116
+ """
117
+ Check if field exists in fields.
118
+ """
119
+ if field not in fields:
120
+ raise ValueError(f"Field '{field}' does not exist in model.")
121
+ return True
122
+
123
+ def __check_fields(self, fields: Optional[Iterable[str]]) -> bool:
124
+ """
125
+ Helper function to check fields specified fields against '__all__'
126
+
127
+ Args:
128
+ - fields (Optional[Iterable[str]]): Fields
129
+
130
+ Returns:
131
+ - True in case all fields otherwise False
132
+ """
133
+ if isinstance(fields, str) and fields == "__all__":
134
+ return True
135
+ if isinstance(fields, list) and len(fields) == 1 and fields[0] == "__all__":
136
+ return True
137
+ return False
138
+
139
+ @property
140
+ def errors(self) -> Optional[list]:
141
+ return self._errors
142
+
143
+
144
+ class Validatable(ABC):
145
+ @property
146
+ def validated_data(self) -> dict:
147
+ pass
148
+
149
+ @abstractmethod
150
+ def is_valid(self) -> bool:
151
+ pass
152
+
153
+ @abstractmethod
154
+ def validate(self) -> bool:
155
+ pass
156
+
157
+
158
+ class Saveable(ABC):
159
+ @abstractmethod
160
+ def save(self) -> object:
161
+ pass
162
+
163
+
164
+ class Updatable(ABC):
165
+ @abstractmethod
166
+ def update(self) -> object:
167
+ pass
168
+
169
+
170
+ class Deletable(ABC):
171
+ @abstractmethod
172
+ def delete(self) -> None:
173
+ pass
174
+
175
+
176
+ class Representable(ABC):
177
+ @abstractmethod
178
+ def to_representation(self, instance: object) -> dict:
179
+ pass
180
+
181
+
182
+ class Creatable(ABC):
183
+ @abstractmethod
184
+ def create(self, validated_data: dict) -> dict:
185
+ pass
186
+
187
+
188
+ class ToInstance(ABC):
189
+ @abstractmethod
190
+ def to_instance(self, data: dict) -> object:
191
+ pass
192
+
193
+
194
+ class ExtendsPydantic(ABC):
195
+ pydantic_model: Optional[BaseModel] = None
196
+
197
+ @abstractmethod
198
+ def validate_pydantic(self: Self, data: dict) -> None:
199
+ pass
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Yuri Boiko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,392 @@
1
+ Metadata-Version: 2.1
2
+ Name: sqlalchemy_serializer_meta
3
+ Version: 0.1.0
4
+ Summary: A simple serializer for SQLAlchemy models and Flask Models
5
+ Home-page: https://github.com/Wehda23/sqlalchemy_serializer_meta
6
+ Author: Waheed Khaled Elhariri
7
+ Author-email: waheedkhaled95@gmail.com
8
+ License: MIT
9
+ Keywords: sqlalchemy serializer pydantic flask
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: sqlalchemy
20
+ Requires-Dist: pydantic ==2.7.4
21
+ Requires-Dist: pydantic-core ==2.18.4
22
+
23
+ # Serializer Module for Flask Application
24
+
25
+ This module provides a set of serializers for your Flask application, allowing you to easily validate and serialize data. The serializers are built with flexibility and extendability in mind, providing features such as nested serialization and integration with Pydantic models.
26
+
27
+ ## Installation
28
+
29
+ To use the serializers in your project, simply include the necessary files in your application directory.
30
+
31
+ ## Usage
32
+
33
+ Can be used to Serialize SQLAlchemy Model or Flask-SQLAlchemy model.
34
+
35
+ ### Basic Usage
36
+
37
+ Here's an example of how to use the `UserRegisterSerializer` to validate and create a new user:
38
+
39
+ ```python
40
+ from your_application.serializers import UserRegisterSerializer
41
+ from your_application.models import User
42
+
43
+ data = {
44
+ "first_name": "John",
45
+ "last_name": "Doe",
46
+ "email": "john.doe@example.com",
47
+ "password": "securepassword123"
48
+ }
49
+
50
+ serializer = UserRegisterSerializer(data=data)
51
+ if serializer.is_valid():
52
+ user = serializer.save()
53
+ print("User created:", user)
54
+ else:
55
+ print("Errors:", serializer.errors)
56
+ ```
57
+
58
+ In this example your custom serializer module/package
59
+
60
+ ```py
61
+ from sqlalchemy_serializer_meta import Serializer, ModelSerializer, SerializerError
62
+ from your_application.models import User
63
+
64
+ class UserRegisterSerializer(Serializer):
65
+ class Meta:
66
+ model = User
67
+ fields = ['first_name', 'last_name', 'email', 'password']
68
+
69
+ def validate_first_name(self, value) -> str:
70
+ # Your Code
71
+ return value
72
+
73
+ class UserSerializer(ModelSerializer):
74
+ class Meta:
75
+ model = User
76
+ fields = ["first_name", "last_name", "email", "password"]
77
+
78
+ class UserLoginSerializer(Serializer):
79
+ class Meta:
80
+ model = User
81
+ fields = ['email', 'password']
82
+
83
+ def validate_email(self, email: str) -> str:
84
+ # Your Code
85
+ # Code pass
86
+ self.instance = object # User Object
87
+ return email
88
+
89
+ def validate_password(self, password: str) -> str:
90
+ # Your Code
91
+ return password
92
+
93
+ # Override serializer
94
+ def to_representation(self, instance: object) -> dict:
95
+ # Your Code
96
+ return UserSerializer(instance=instance).data
97
+ ```
98
+
99
+ ### Nested Serialization
100
+
101
+ To handle nested relationships, you can define nested serializers and use them in your main serializer.
102
+
103
+ #### Single Nested Object
104
+
105
+ When dealing with a single nested object, you do not need to specify `many=True`. Here's an example:
106
+
107
+ ```python
108
+ from your_application.serializers import UserSerializer, ShippingAddressSerializer
109
+
110
+ class UserDetailSerializer(UserSerializer):
111
+ shipping_address = ShippingAddressSerializer()
112
+
113
+ class Meta:
114
+ model = User
115
+ fields = ['id', 'first_name', 'last_name', 'email', 'shipping_address']
116
+
117
+ # Usage
118
+ user = User.query.get(1)
119
+ serializer = UserDetailSerializer(instance=user)
120
+ print(serializer.data)
121
+ ```
122
+
123
+ This will serialize the `shipping_address` field as a dictionary:
124
+
125
+ ```json
126
+ {
127
+ "id": 1,
128
+ "first_name": "John",
129
+ "last_name": "Doe",
130
+ "email": "john.doe@example.com",
131
+ "shipping_address": {
132
+ "street": "123 Main St",
133
+ "city": "Anytown",
134
+ "zipcode": "12345"
135
+ }
136
+ }
137
+ ```
138
+
139
+ #### Multiple Nested Objects
140
+
141
+ When dealing with multiple nested objects, use `many=True` to return a list of dictionaries. Here's an example:
142
+
143
+ ```python
144
+ from your_application.serializers import UserSerializer, ShippingAddressSerializer
145
+
146
+ class UserDetailSerializer(UserSerializer):
147
+ shipping_addresses = ShippingAddressSerializer(many=True)
148
+
149
+ class Meta:
150
+ model = User
151
+ fields = ['id', 'first_name', 'last_name', 'email', 'shipping_addresses']
152
+
153
+ # Usage
154
+ user = User.query.get(1)
155
+ serializer = UserDetailSerializer(instance=user)
156
+ print(serializer.data)
157
+ ```
158
+
159
+ This will serialize the `shipping_addresses` field as a list of dictionaries:
160
+
161
+ ```json
162
+ {
163
+ "id": 1,
164
+ "first_name": "John",
165
+ "last_name": "Doe",
166
+ "email": "john.doe@example.com",
167
+ "shipping_addresses": [
168
+ {
169
+ "street": "123 Main St",
170
+ "city": "Anytown",
171
+ "zipcode": "12345"
172
+ },
173
+ {
174
+ "street": "456 Elm St",
175
+ "city": "Othertown",
176
+ "zipcode": "67890"
177
+ }
178
+ ]
179
+ }
180
+ ```
181
+
182
+ ### Integration with Pydantic Models
183
+
184
+ You can also integrate Pydantic models for additional validation. Here's an example:
185
+
186
+ ```python
187
+ from your_application.serializers import UserSerializer, SerializerError
188
+ from pydantic import BaseModel, EmailStr, ValidationError
189
+
190
+ class UserPydanticModel(BaseModel):
191
+ first_name: str
192
+ last_name: str
193
+ email: EmailStr
194
+
195
+ class UserPydanticSerializer(UserSerializer):
196
+ pydantic_model = UserPydanticModel
197
+
198
+ def validate_pydantic(self, data):
199
+ try:
200
+ pydantic_instance = self.pydantic_model(**data)
201
+ return pydantic_instance.model_dump() # Dump the validated model to dictionary
202
+ except ValidationError as e:
203
+ raise SerializerError(e, "Invalid data")
204
+
205
+ # Usage
206
+ data = {
207
+ "first_name": "John",
208
+ "last_name": "Doe",
209
+ "email": "john.doe@example.com"
210
+ }
211
+
212
+ serializer = UserPydanticSerializer(data=data)
213
+ if serializer.is_valid():
214
+ print("Valid data:", serializer.validated_data)
215
+ else:
216
+ print("Errors:", serializer.errors)
217
+ ```
218
+
219
+ ### Updating Existing Instances
220
+
221
+ The `.save()` method is designed to update an instance if it already exists, rather than creating a new one. If the `instance` attribute is set, `.save()` will call the `.update()` method. If `instance` is `None`, it will call `.create()`.
222
+
223
+ Here's an example:
224
+
225
+ ```python
226
+ from your_application.serializers import UserSerializer
227
+ from your_application.models import User
228
+
229
+ # Update an existing user
230
+ user_instance = User.query.get(1)
231
+ data = {
232
+ "first_name": "Jane",
233
+ "last_name": "Doe",
234
+ "email": "jane.doe@example.com"
235
+ }
236
+
237
+ serializer = UserSerializer(instance=user_instance, data=data)
238
+ if serializer.is_valid():
239
+ user = serializer.save()
240
+ print("User updated:", user)
241
+ else:
242
+ print("Errors:", serializer.errors)
243
+ ```
244
+
245
+ ## Custom Serializers
246
+
247
+ You can create custom serializers by inheriting from `Serializer` or `ModelSerializer`. Here's an example:
248
+
249
+ ```python
250
+ from sqlalchemy_serializer_meta import ModelSerializer
251
+ from your_application.models import CustomModel
252
+
253
+ class CustomModelSerializer(ModelSerializer):
254
+ class Meta:
255
+ model = CustomModel
256
+ fields = ['id', 'name', 'description']
257
+
258
+ # Usage
259
+ instance = CustomModel.query.get(1)
260
+ serializer = CustomModelSerializer(instance=instance)
261
+ print(serializer.data)
262
+ ```
263
+
264
+ ## Serializer Validated Fields
265
+
266
+ You can customize your field validation by adding `validate_<field_name>` methods in your `Serializer class`.
267
+
268
+ Can be used in SQLAlchemy and Flask API
269
+
270
+ ```py
271
+ from sqlalchemy_serializer_meta import Serializer
272
+ from your_application.models import CustomModel
273
+ from your_application.validators import CustomValidator
274
+ from typing import Union
275
+
276
+ class CustomPydantic(BaseModel):
277
+ id: str
278
+ name: str
279
+ description: EmailStr
280
+
281
+ class CustomModelSerializer(Serializer):
282
+ # If not specified the validate_pydantic method won't be called hence no errors will occure
283
+ pydantic_model: BaseModel = CustomPydantic
284
+
285
+ class Meta:
286
+ model = CustomModel
287
+ fields = ['id', 'name', 'description']
288
+
289
+ # Custom Validation for name
290
+ def validate_name(self, value):
291
+ # This will trigger Custome made Validator or pass
292
+ CustomValidator(SerializerError).validate(value)
293
+ return value
294
+
295
+
296
+ def serialize(data: dict) -> Union[dict, list[dict]]:
297
+ # To trigger validators you have to initiate .is_valid() method
298
+ serializer = CustomModelSerializer(data=data)
299
+
300
+ try:
301
+ # Trigger validation
302
+ if serializer.is_valid():
303
+ serializer.save()
304
+ return serializer.data
305
+ # If validation did not pass custom validation Grab Errors
306
+ return serializer.errors
307
+ # if validation did not pass Pydantic Validation
308
+ except ValidationError as e: # If pydanic model not defined this error will not occure because validate_pydantic wont be called.
309
+ return e.errors()
310
+ # other unexpected or unhandled errors.
311
+ except Exception as e:
312
+ return e
313
+
314
+ serialize({'name': 'test', 'description': 'test@test.com',"id":1})
315
+ ```
316
+
317
+ ### Reusable Serializer Design with SQLAlchemy and Flask-SQLAlchemy
318
+
319
+ The design of the `CustomModelSerializer` class in our Flask application leverages the flexibility and robustness of SQLAlchemy and Flask-SQLAlchemy, allowing it to be highly reusable across different projects. This approach ensures that the main views or functions in your API can remain focused on returning data or handling errors, rather than getting bogged down in validation logic.
320
+
321
+ #### Key Features of the Design:
322
+
323
+ 1. **Modular Validation:**
324
+ - By using Pydantic for model validation, we ensure that data is consistently validated before it is processed or saved.
325
+ - Custom validation methods like `validate_name` allow for specific field-level validation rules, making the serializer adaptable to different models and requirements.
326
+
327
+ 2. **Separation of Concerns:**
328
+ - The serializer handles data validation, deserialization, and serialization, keeping these concerns separate from the view logic.
329
+ - This separation allows the view functions to focus on API response generation, improving readability and maintainability.
330
+
331
+ 3. **Custom Error Handling:**
332
+ - The design includes robust error handling, catching validation errors from Pydantic and custom validators, and other exceptions.
333
+ - This ensures that the API can provide meaningful error messages to the client, improving the user experience and making debugging easier.
334
+
335
+ 4. **Ease of Integration:**
336
+ - The `CustomModelSerializer` can be easily integrated with Flask routes, as demonstrated in the example.
337
+ - This flexibility makes it simple to reuse the serializer across different endpoints or even different projects, as long as they use SQLAlchemy and Flask-SQLAlchemy.
338
+
339
+ #### Example Usage in Flask API:
340
+
341
+ ```py
342
+ from flask import Flask, request, jsonify
343
+ from sqlalchemy_serializer_meta import Serializer
344
+ from your_application.models import CustomModel
345
+ from your_application.validators import CustomValidator
346
+ from pydantic import BaseModel, EmailStr, ValidationError
347
+ from typing import Union
348
+
349
+ class CustomPydantic(BaseModel):
350
+ id: str
351
+ name: str
352
+ description: EmailStr
353
+
354
+ class CustomModelSerializer(Serializer):
355
+ pydantic_model: BaseModel = CustomPydantic
356
+
357
+ class Meta:
358
+ model = CustomModel
359
+ fields = ['id', 'name', 'description']
360
+
361
+ def validate_name(self, value):
362
+ CustomValidator(SerializerError).validate(value)
363
+ return value
364
+
365
+ app = Flask(__name__)
366
+
367
+ @app.route('/serialize', methods=['POST'])
368
+ def serialize_data():
369
+ data = request.json
370
+ serializer = CustomModelSerializer(data=data)
371
+
372
+ try:
373
+ if serializer.is_valid():
374
+ serializer.save()
375
+ return jsonify(serializer.data), 201
376
+ return jsonify(serializer.errors), 400
377
+ except ValidationError as e:
378
+ return jsonify(e.errors()), 422
379
+ except Exception as e:
380
+ return jsonify({"error": str(e)}), 500
381
+
382
+ if __name__ == '__main__':
383
+ app.run(debug=True)
384
+ ```
385
+
386
+ #### Benefits for Different Projects:
387
+
388
+ - **Consistency:** Ensures consistent data validation across various projects, reducing the likelihood of errors and inconsistencies.
389
+ - **Reusability:** The modular design means the same serializer class can be used across different models and endpoints, minimizing duplicate code.
390
+ - **Focus on Business Logic:** By handling validation and serialization within the serializer, the main view functions can focus on business logic and API response handling, making the codebase cleaner and more maintainable.
391
+
392
+ By adopting this design, you can create scalable, maintainable, and robust APIs that leverage the power of SQLAlchemy and Flask-SQLAlchemy, ensuring that your application remains flexible and adaptable to changing requirements.
@@ -0,0 +1,7 @@
1
+ sqlalchemy_serializer_meta/__init__.py,sha256=R0tgVeUHxXSuV8ET8KTKDKFstS2h6iCaE7JvVMFpmF4,14503
2
+ sqlalchemy_serializer_meta/serializer.py,sha256=lr0MOZmMT5Ynf89oLUJtLKQYrId674xRbfAVdaWb2DY,5782
3
+ sqlalchemy_serializer_meta-0.1.0.dist-info/LICENSE,sha256=c1dx0gfbut5pHGHayRypYfy9dQjP5zGcINW3OvzNIQo,1086
4
+ sqlalchemy_serializer_meta-0.1.0.dist-info/METADATA,sha256=W7COlzr2Qvqlz1P6ZpW2KaXVaFYml-2UYNLyCoAD_8Q,13204
5
+ sqlalchemy_serializer_meta-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ sqlalchemy_serializer_meta-0.1.0.dist-info/top_level.txt,sha256=rlJUoEf220tqtRa1uvL41fVE9MrRuenX6nYjyTEAbYM,27
7
+ sqlalchemy_serializer_meta-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sqlalchemy_serializer_meta