ab-pydantic-patch 1.2.1__tar.gz → 1.2.2__tar.gz

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 (58) hide show
  1. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/PKG-INFO +1 -1
  2. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/pyproject.toml +1 -1
  3. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/orm_patch.py +12 -6
  4. ab_pydantic_patch-1.2.2/src/ab_core/pydantic_patch/pydantic_jsonb.py +98 -0
  5. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/.gitignore +0 -0
  6. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/LICENSE +0 -0
  7. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/README.md +0 -0
  8. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/__init__.py +0 -0
  9. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/__init__.py +0 -0
  10. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/cache.py +0 -0
  11. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/classproperty.py +0 -0
  12. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/computed_field_type_hints.py +0 -0
  13. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/config.py +0 -0
  14. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/errors.py +0 -0
  15. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/field_type_hints.py +0 -0
  16. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/fields.py +0 -0
  17. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/forward_references.py +0 -0
  18. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/operation.py +0 -0
  19. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/orm_type_hints.py +0 -0
  20. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/payload.py +0 -0
  21. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/payload_types.py +0 -0
  22. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/transform.py +0 -0
  23. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/type_hints.py +0 -0
  24. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/core/types.py +0 -0
  25. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/__init__.py +0 -0
  26. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/pydantic_examples/__init__.py +0 -0
  27. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/pydantic_examples/pydantic_computed_fields.py +0 -0
  28. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/pydantic_examples/pydantic_discriminated_union_api_schema.py +0 -0
  29. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/__init__.py +0 -0
  30. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/app_broken.py +0 -0
  31. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/app_resolved.py +0 -0
  32. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/__init__.py +0 -0
  33. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/project.py +0 -0
  34. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/project_milestone.py +0 -0
  35. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/project_task.py +0 -0
  36. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/task_comment.py +0 -0
  37. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/models/user.py +0 -0
  38. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/examples/sqlmodel_examples/sqlmodel_computed_fields.py +0 -0
  39. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/omit/__init__.py +0 -0
  40. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/omit/api.py +0 -0
  41. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/omit/config.py +0 -0
  42. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/omit/operation.py +0 -0
  43. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/partial/__init__.py +0 -0
  44. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/partial/api.py +0 -0
  45. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/partial/config.py +0 -0
  46. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/partial/operation.py +0 -0
  47. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/patch/__init__.py +0 -0
  48. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/patch/api.py +0 -0
  49. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/patch/config.py +0 -0
  50. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/patch/operation.py +0 -0
  51. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/pick/__init__.py +0 -0
  52. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/pick/api.py +0 -0
  53. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/pick/config.py +0 -0
  54. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/pick/operation.py +0 -0
  55. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/required/__init__.py +0 -0
  56. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/required/api.py +0 -0
  57. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/required/config.py +0 -0
  58. {ab_pydantic_patch-1.2.1 → ab_pydantic_patch-1.2.2}/src/ab_core/pydantic_patch/required/operation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ab-pydantic-patch
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Python Pydantic support of TypeScript-style utility types, including Partial, Required, Pick, and Omit. Useful for PATCH endpoints driven from BaseModel / SQLModel classes.
5
5
  Author-email: Matt Coulter <mattcoul7@gmail.com>
6
6
  License-File: LICENSE
@@ -28,7 +28,7 @@ description = "Python Pydantic support of TypeScript-style utility types, includ
28
28
  name = "ab-pydantic-patch"
29
29
  readme = "README.md"
30
30
  requires-python = ">=3.12,<4.0"
31
- version = "1.2.1"
31
+ version = "1.2.2"
32
32
 
33
33
  [project.optional-dependencies]
34
34
  orm = [
@@ -27,6 +27,16 @@ def _provided_values(model: BaseModel) -> dict[str, object]:
27
27
  return {name: getattr(model, name) for name in model.model_fields_set}
28
28
 
29
29
 
30
+ def _patch_scalar_value(instance: Any, key: str, value: Any) -> None:
31
+ current_value = getattr(instance, key, None)
32
+
33
+ if isinstance(current_value, BaseModel) and isinstance(value, BaseModel):
34
+ recursive_patch_scalar(current_value, value)
35
+ return
36
+
37
+ setattr(instance, key, value)
38
+
39
+
30
40
  def _mapper_for(model_cls: type[Any]) -> Any | None:
31
41
  if sa_inspect is None:
32
42
  return None
@@ -92,15 +102,11 @@ def _recursive_patch_pydantic_scalar(
92
102
 
93
103
  current_value = getattr(instance, key, None)
94
104
 
95
- if isinstance(current_value, BaseModel) and isinstance(value, BaseModel):
96
- recursive_patch_scalar(current_value, value)
97
- continue
98
-
99
105
  if isinstance(current_value, list) and isinstance(value, list):
100
106
  setattr(instance, key, value)
101
107
  continue
102
108
 
103
- setattr(instance, key, value)
109
+ _patch_scalar_value(instance, key, value)
104
110
 
105
111
 
106
112
  def _recursive_patch_orm_scalar(
@@ -123,7 +129,7 @@ def _recursive_patch_orm_scalar(
123
129
  if relationship is None:
124
130
  if key not in scalar_attributes:
125
131
  continue
126
- setattr(orm_instance, key, value)
132
+ _patch_scalar_value(orm_instance, key, value)
127
133
  continue
128
134
 
129
135
  if value is None:
@@ -0,0 +1,98 @@
1
+ """Support Pydantic Models as a JSON Serialisable Column in Postgres.
2
+
3
+ As per: https://github.com/fastapi/sqlmodel/issues/63
4
+
5
+ This is relatively common requirement that sqlmodel has not implemented for us :(
6
+
7
+ Basically, we don't always want to use relationships and overcomplicate the ORM. Instead,
8
+ we just wwant to serialise the pydantic dependency as JSON withhin that column.
9
+
10
+ However, if we use JSONB directly, it requires that we define
11
+
12
+ supplier_address: dict = Field(
13
+ default_factory=SupplierAddress,
14
+ description="Supplier business address extracted from the quote.",
15
+ sa_column=Column(JSONB, nullable=False),
16
+ )
17
+
18
+ Which isn't clean, and no long type-driven.
19
+
20
+ To preserve the type driven nature for these cases, PydanticJSONB is our friend:
21
+
22
+ supplier_address: SupplierAddress = Field(
23
+ default_factory=SupplierAddress,
24
+ description="Supplier business address extracted from the quote.",
25
+ sa_column=Column(PydanticJSONB(SupplierAddress), nullable=False),
26
+ )
27
+
28
+ TODO: This does cause some incompaibility with alembic (I think), so we will need
29
+ to address that once alembic is added to this codebase. It looks something like this:
30
+
31
+ ```python
32
+ from typing import TYPE_CHECKING, Any, Literal
33
+ from knowledgeburst.fencing.quote_stateful.pydantic_jsonb import PydanticJSONB
34
+
35
+ if TYPE_CHECKING:
36
+ from alembic.autogenerate.api import AutogenContext
37
+
38
+ def render_item(
39
+ type_: str,
40
+ obj: Any, # noqa: ANN401
41
+ autogen_context: AutogenContext,
42
+ ) -> str | Literal[False]:
43
+ if type_ == "type" and isinstance(obj, PydanticJSONB):
44
+ autogen_context.imports.add("import sqlalchemy as sa")
45
+ autogen_context.imports.add("from sqlalchemy.dialects import postgresql")
46
+ return "postgresql.JSONB(astext_type=sa.Text())"
47
+ return False
48
+ ```
49
+
50
+ In that case, we probably want to move this module into a more central db module. Right now
51
+ it is just coupled with fencing as we are keeping the fencing module as independent as possible.
52
+ """
53
+
54
+ from typing import Any, cast, override
55
+
56
+ from pydantic import TypeAdapter
57
+ from sqlalchemy import Dialect, TypeDecorator
58
+ from sqlalchemy.dialects.postgresql import JSONB
59
+
60
+
61
+ class PydanticJSONB[T](TypeDecorator):
62
+ """A SQLAlchemy TypeDecorator that serializes/deserializes Pydantic models to/from JSONB.
63
+
64
+ This allows storing Pydantic models in PostgreSQL JSONB columns while maintaining
65
+ type safety and validation.
66
+ """
67
+
68
+ impl = JSONB
69
+ cache_ok = True
70
+
71
+ def __init__(self, pydantic_type: type[T]) -> None:
72
+ """Initialize the PydanticJSONB type decorator.
73
+
74
+ Args:
75
+ pydantic_type: The Pydantic model class to serialize/deserialize.
76
+
77
+ """
78
+ super().__init__()
79
+ self.adapter = TypeAdapter(pydantic_type)
80
+
81
+ @override
82
+ def process_bind_param(self, value: T | None, dialect: Dialect) -> Any:
83
+ if value is None:
84
+ return None
85
+ return self.adapter.dump_python(value, mode="json")
86
+
87
+ @override
88
+ def process_result_value(self, value: Any, dialect: Dialect) -> T | None:
89
+ if value is None:
90
+ return None
91
+ return self.adapter.validate_python(value)
92
+
93
+ def coerce_compared_value(self, op: Any, value: Any) -> Any:
94
+ """Coerce the compared value for SQL operations.
95
+
96
+ Delegates to the underlying JSONB implementation for comparison operations.
97
+ """
98
+ return cast(JSONB, self.impl).coerce_compared_value(op, value)