This is an automated email from the ASF dual-hosted git repository.

Gerrrr pushed a commit to branch pydantic-and-split-persistence-report-json
in repository https://gitbox.apache.org/repos/asf/otava.git

commit 31886b03b0db9d37a8686996920901bdfda532fb
Author: Alex Sorokoumov <[email protected]>
AuthorDate: Tue Aug 18 19:52:58 2026 -0700

    Split persistence and report JSON serialization
    
    Make domain to_json() methods return the precise persistence representation:
    ChangePoint and ChangePointGroup now emit numeric fields, include qhat, and 
no
    longer use rounded=True/False as a presentation switch.
    
    Move rounded JSON output into Report, alongside the other report renderers.
    ReportType.JSON now owns display-only fields such as 
backward_change_percent and
    formats human-facing values as strings.
    
    Add Pydantic v2 schema models for the persisted analyzed-series payload and
    validate AnalyzedSeries.to_json() against that schema before returning 
JSON-safe
    data.
    
    Update tests to pin the split: persistence JSON stays numeric and
    round-trippable, while report JSON keeps the existing rounded output shape.
---
 otava/change_point_divisive/base.py | 127 ++++++++++++++---------------
 otava/report.py                     |  32 +++++++-
 otava/serialization.py              |  68 ++++++++++++++++
 otava/series.py                     |  27 +++++--
 pyproject.toml                      |   1 +
 tests/change_point_classes_test.py  |  38 ++++++---
 tests/report_test.py                |  88 ++++++++++++++-------
 tests/series_test.py                |  31 ++++++++
 uv.lock                             | 154 ++++++++++++++++++++++++++++++++++++
 9 files changed, 454 insertions(+), 112 deletions(-)

diff --git a/otava/change_point_divisive/base.py 
b/otava/change_point_divisive/base.py
index 1020c83..edc0f61 100644
--- a/otava/change_point_divisive/base.py
+++ b/otava/change_point_divisive/base.py
@@ -201,9 +201,8 @@ class ChangePoint(CandidateChangePoint, 
Generic[GenericStats]):
         data = {f.name: getattr(self, f.name) for f in 
fields(CandidateChangePoint)}
         return CandidateChangePoint(**data)
 
-    def to_json(self, rounded=True):
-        cps = ChangePointSerializer(self)
-        return cps.to_json(rounded)
+    def to_json(self):
+        return ChangePointSerializer(self).to_json()
 
 
 class ChangePointSerializer(ChangePoint):
@@ -217,6 +216,7 @@ class ChangePointSerializer(ChangePoint):
     def __init__(self, cp: ChangePoint[GenericStats]):
         self.stats = cp.stats
         self.index = cp.index
+        self.qhat = cp.qhat
         self.metric = cp.metric
 
     def forward_change_percent(self) -> float:
@@ -243,33 +243,19 @@ class ChangePointSerializer(ChangePoint):
     def pvalue(self):
         return self.stats.pvalue
 
-    def to_json(self, rounded=True):
-        if rounded:
-            return {
-                "metric": self.metric,
-                "index": int(self.index),
-                "forward_change_percent": 
f"{self.forward_change_percent():.0f}",
-                "backward_change_percent": 
f"{self.backward_change_percent():.0f}",
-                "magnitude": f"{self.magnitude():-0f}",
-                "mean_before": f"{self.mean_before():-0f}",
-                "stddev_before": f"{self.stddev_before():-0f}",
-                "mean_after": f"{self.mean_after():-0f}",
-                "stddev_after": f"{self.stddev_after():-0f}",
-                "pvalue": f"{self.pvalue():-0f}",
-            }
-
-        else:
-            return {
-                "metric": self.metric,
-                "index": int(self.index),
-                "forward_change_percent": self.forward_change_percent(),
-                "magnitude": self.magnitude(),
-                "mean_before": self.mean_before(),
-                "stddev_before": self.stddev_before(),
-                "mean_after": self.mean_after(),
-                "stddev_after": self.stddev_after(),
-                "pvalue": self.pvalue(),
-            }
+    def to_json(self):
+        return {
+            "metric": self.metric,
+            "index": int(self.index),
+            "qhat": self.qhat,
+            "forward_change_percent": self.forward_change_percent(),
+            "magnitude": self.magnitude(),
+            "mean_before": self.mean_before(),
+            "stddev_before": self.stddev_before(),
+            "mean_after": self.mean_after(),
+            "stddev_after": self.stddev_after(),
+            "pvalue": self.pvalue(),
+        }
 
 
 @dataclass
@@ -288,15 +274,16 @@ class ChangePointGroup:
     :param attributes: The attributes of the test result at this timestamp. 
Commit and test metadata.
     :param changes: For each metric that has a change point at this 
time(stamp), the ChangePoint object.
     """
+
     time: float
     attributes: Dict[str, str]
     # ChangePointGroup.changes.keys() stores the set of metrics that were used 
at this ChangePointGroup.time.
     changes: Dict[str, ChangePoint]
 
-    def to_json(self, rounded=False):
+    def to_json(self):
         changes = []
         for metric, cp in self.changes.items():
-            changes.append(cp.to_json(rounded=rounded))
+            changes.append(cp.to_json())
 
         return {
             "time": self.time,
@@ -365,9 +352,7 @@ class ChangePoints:
     def from_list(cls, cps: list[ChangePointGroup]):
         """Build from a list of ChangePointGroup objects, ordered by time."""
         if not isinstance(cps, list):
-            raise TypeError(
-                f"from_list() argument must be a list. Got {type(cps)}."
-            )
+            raise TypeError(f"from_list() argument must be a list. Got 
{type(cps)}.")
         for cpg in cps:
             if not isinstance(cpg, ChangePointGroup):
                 raise TypeError(
@@ -396,12 +381,12 @@ class ChangePoints:
         for metric, cpglist in cps.items():
             for cpg in cpglist:
                 if not isinstance(cpg, ChangePointGroup):
-                    raise TypeError(
-                        "from_dict() takes a dict[str, 
list[ChangePointGroup]]."
-                    )
+                    raise TypeError("from_dict() takes a dict[str, 
list[ChangePointGroup]].")
                 for cp in cpg:
                     if cp.metric and cp.metric != metric:
-                        raise ValueError(f"metric field is not internally 
consistent. {cp.metric} != {metric} at {cpg.time}")
+                        raise ValueError(
+                            f"metric field is not internally consistent. 
{cp.metric} != {metric} at {cpg.time}"
+                        )
             # store each metric's groups sorted by timestamp
             obj._change_points[metric] = sorted(cpglist, key=lambda cpg: 
cpg.time)
         return obj
@@ -506,7 +491,9 @@ class ChangePoints:
         for cpg in single_metric._change_points:
             for metric, cp in cpg.changes.items():
                 if cp.metric and cp.metric != metric:
-                    raise ValueError(f"metric field is not internally 
consistent. {cp.metric} != {metric} at {cpg.time}")
+                    raise ValueError(
+                        f"metric field is not internally consistent. 
{cp.metric} != {metric} at {cpg.time}"
+                    )
                 metric_change_points.append(cp)
         return metric_change_points
 
@@ -520,7 +507,7 @@ class ChangePoints:
 
     def at_commit(self, sha: str):
         for row in self:
-            if row.attributes['commit'] == sha:
+            if row.attributes["commit"] == sha:
                 return row
         raise LookupError(sha)
 
@@ -539,17 +526,18 @@ class ChangePoints:
 
 class ChangePointsByTime(ChangePoints):
     """
-        Implementation of ChangePoints class where the internal structure is 
ordered by time/commit.
+    Implementation of ChangePoints class where the internal structure is 
ordered by time/commit.
 
-        In fact, this is the default way, and therefore all of this class' 
implementation is already
-        in its parent class ChangePoints. However, you can still create 
instances from this class
-        to make it explicit that your code at that point explicitly wanted a 
collection of ChangePoints
-        ordered by time.
+    In fact, this is the default way, and therefore all of this class' 
implementation is already
+    in its parent class ChangePoints. However, you can still create instances 
from this class
+    to make it explicit that your code at that point explicitly wanted a 
collection of ChangePoints
+    ordered by time.
 
-        The pivot() method will return a new object (a copy) holding the same 
data, but ordered by metrics
-        as the primary and optimized axis. The method by_metric() can be used 
for the same purpose. Note
-        that the method by_time() is a no-op and returns self, it doesn't even 
do a copy.
+    The pivot() method will return a new object (a copy) holding the same 
data, but ordered by metrics
+    as the primary and optimized axis. The method by_metric() can be used for 
the same purpose. Note
+    that the method by_time() is a no-op and returns self, it doesn't even do 
a copy.
     """
+
     @classmethod
     def from_dict(cls, cps: dict):
         """
@@ -563,14 +551,14 @@ class ChangePointsByTime(ChangePoints):
 
 class ChangePointsByMetric(ChangePoints):
     """
-        Provides same interface as ChangePointsByTime, but internally stores 
with metric first.
+    Provides same interface as ChangePointsByTime, but internally stores with 
metric first.
 
-        You can create empty instances of this class, or you can also use the 
factory method
-        `ChangePoints.from_dict()` to get an instance of this type.
+    You can create empty instances of this class, or you can also use the 
factory method
+    `ChangePoints.from_dict()` to get an instance of this type.
 
-        The pivot() method will return a new object (a copy) holding the same 
data, but ordered by time
-        as the primary and optimized axis. The method by_time() can be used 
for the same purpose. Note
-        that the method by_metric() is a no-op and returns self, it doesn't 
even do a copy.
+    The pivot() method will return a new object (a copy) holding the same 
data, but ordered by time
+    as the primary and optimized axis. The method by_time() can be used for 
the same purpose. Note
+    that the method by_metric() is a no-op and returns self, it doesn't even 
do a copy.
     """
 
     def __init__(self):
@@ -620,7 +608,9 @@ class ChangePointsByMetric(ChangePoints):
                 self._change_points[metric] = []
             for metric1, cp in cpg.changes.items():
                 if metric1 != cp.metric:
-                    raise ValueError(f"metric field is not internally 
consistent. {metric1} != {cp.metric} at {cpg.time}")
+                    raise ValueError(
+                        f"metric field is not internally consistent. {metric1} 
!= {cp.metric} at {cpg.time}"
+                    )
             if (not self._change_points[metric]) or cpg.time > 
self._change_points[metric][-1].time:
                 self._change_points[metric].append(cpg.select_metrics(metric))
             else:
@@ -637,10 +627,14 @@ class ChangePointsByMetric(ChangePoints):
                 raise TypeError(errmsg)
             for metric1, cp in cpg.changes.items():
                 if metric1 != cp.metric:
-                    raise ValueError(f"metric field is not internally 
consistent. {metric1} != {cp.metric} at {cpg.time}")
+                    raise ValueError(
+                        f"metric field is not internally consistent. {metric1} 
!= {cp.metric} at {cpg.time}"
+                    )
                 if metric1 not in self._change_points:
                     self._change_points[metric1] = []
-                if (not self._change_points[metric1]) or cpg.time > 
self._change_points[metric1][-1].time:
+                if (not self._change_points[metric1]) or cpg.time > 
self._change_points[metric1][
+                    -1
+                ].time:
                     self._change_points[metric1].append(cpg)
                 else:
                     raise ValueError(
@@ -706,7 +700,9 @@ class ChangePointsByMetric(ChangePoints):
             for cpg in cpglist:
                 for metric2, cp in cpg.changes.items():
                     if metric1 != metric2 or cp.metric and metric1 != 
cp.metric:
-                        raise ValueError(f"metric field is not internally 
consistent. {metric1} != {cp.metric} at {cpg.time}")
+                        raise ValueError(
+                            f"metric field is not internally consistent. 
{metric1} != {cp.metric} at {cpg.time}"
+                        )
                     metric_change_points.append(cp)
 
         return metric_change_points
@@ -730,13 +726,18 @@ class SignificanceTester(Generic[GenericStats]):
     def __init__(self, max_pvalue: float):
         self.max_pvalue = max_pvalue
 
-    def compare(self, left: Sequence[SupportsFloat], right: 
Sequence[SupportsFloat], p: float = None) -> GenericStats:
+    def compare(
+        self, left: Sequence[SupportsFloat], right: Sequence[SupportsFloat], 
p: float = None
+    ) -> GenericStats:
         if len(left) == 0 or len(right) == 0:
             raise ValueError
         return BaseStats.calculate(left, right, p)
 
     def get_sides(
-        self, candidate: CandidateChangePoint, series: 
Sequence[SupportsFloat], intervals: List[slice]
+        self,
+        candidate: CandidateChangePoint,
+        series: Sequence[SupportsFloat],
+        intervals: List[slice],
     ) -> (Sequence, Sequence):
         """
         Computes properties of the change point if the Candidate Change Point 
based on the provided intervals.
@@ -757,7 +758,9 @@ class SignificanceTester(Generic[GenericStats]):
                 left_interval = interval
                 right_interval = intervals[i + 1]
                 break
-            elif (interval.start is None or interval.start < candidate.index) 
and (interval.stop is None or candidate.index < interval.stop):
+            elif (interval.start is None or interval.start < candidate.index) 
and (
+                interval.stop is None or candidate.index < interval.stop
+            ):
                 # Split step
                 # Note: handles slices with omitted indexes:
                 #
diff --git a/otava/report.py b/otava/report.py
index 5431b82..e46db72 100644
--- a/otava/report.py
+++ b/otava/report.py
@@ -103,7 +103,37 @@ class Report:
     def __format_json(self, test_name: str) -> str:
         import json
 
-        return json.dumps({test_name: [cpg.to_json(rounded=True) for cpg in 
self.__change_points]})
+        return json.dumps(
+            {
+                test_name: [
+                    self.__format_change_point_group_json(cpg) for cpg in 
self.__change_points
+                ]
+            }
+        )
+
+    @staticmethod
+    def __format_change_point_group_json(cpg):
+        return {
+            "time": cpg.time,
+            "attributes": cpg.attributes,
+            "changes": [Report.__format_change_point_json(cp) for cp in 
cpg.changes.values()],
+        }
+
+    @staticmethod
+    def __format_change_point_json(cp):
+        cp = ChangePointSerializer(cp)
+        return {
+            "metric": cp.metric,
+            "index": int(cp.index),
+            "forward_change_percent": f"{cp.forward_change_percent():.0f}",
+            "backward_change_percent": f"{cp.backward_change_percent():.0f}",
+            "magnitude": f"{cp.magnitude():-0f}",
+            "mean_before": f"{cp.mean_before():-0f}",
+            "stddev_before": f"{cp.stddev_before():-0f}",
+            "mean_after": f"{cp.mean_after():-0f}",
+            "stddev_after": f"{cp.stddev_after():-0f}",
+            "pvalue": f"{cp.pvalue():-0f}",
+        }
 
     def __format_regressions_only(self, test_name: str) -> str:
         output = []
diff --git a/otava/serialization.py b/otava/serialization.py
new file mode 100644
index 0000000..8ed4546
--- /dev/null
+++ b/otava/serialization.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from datetime import datetime
+from typing import Dict, List, Optional
+
+from pydantic import BaseModel, ConfigDict
+
+
+class AnalysisOptionsModel(BaseModel):
+    window_len: int
+    max_pvalue: float
+    min_magnitude: float
+    orig_edivisive: bool
+
+
+class MetricModel(BaseModel):
+    direction: Optional[int] = None
+    scale: Optional[float] = None
+    unit: str = ""
+
+
+class ChangePointModel(BaseModel):
+    metric: Optional[str] = None
+    index: int
+    qhat: float
+    forward_change_percent: float
+    magnitude: float
+    mean_before: float
+    stddev_before: float
+    mean_after: float
+    stddev_after: float
+    pvalue: float
+
+
+class ChangePointGroupModel(BaseModel):
+    time: float
+    attributes: Dict[str, str]
+    changes: List[ChangePointModel]
+
+
+class AnalyzedSeriesModel(BaseModel):
+    model_config = ConfigDict(arbitrary_types_allowed=False)
+
+    test_name: str
+    time: List[int]
+    change_points_timestamp: datetime
+    branch_name: Optional[str] = None
+    options: AnalysisOptionsModel
+    metrics: Dict[str, MetricModel]
+    attributes: Dict[str, List[str]]
+    data: Dict[str, List[Optional[float]]]
+    change_points: Dict[str, List[ChangePointGroupModel]]
+    weak_change_points: Dict[str, List[ChangePointGroupModel]]
diff --git a/otava/series.py b/otava/series.py
index e3d9e82..dbc6822 100644
--- a/otava/series.py
+++ b/otava/series.py
@@ -32,6 +32,7 @@ from otava.change_point_divisive.base import (
     ChangePointsByMetric,
     ChangePointsByTime,
 )
+from otava.serialization import AnalyzedSeriesModel
 
 
 @dataclass
@@ -371,31 +372,36 @@ class AnalyzedSeries:
         for metric_name in self.change_points.metrics():
             change_points_json[metric_name] = []
             for cp in cpbm.select_metrics(metric_name):
-                
change_points_json[metric_name].append(cp.to_json(rounded=False))
+                change_points_json[metric_name].append(cp.to_json())
 
         weak_change_points_json = {}
         wcpbm = self.weak_change_points.by_metric()
         for metric_name in self.weak_change_points.metrics():
             weak_change_points_json[metric_name] = []
             for cp in wcpbm.select_metrics(metric_name):
-                
weak_change_points_json[metric_name].append(cp.to_json(rounded=False))
+                weak_change_points_json[metric_name].append(cp.to_json())
 
         data_json = {}
         for metric, datapoints in self.__series.data.items():
             data_json[metric] = [float(d) if d is not None else None for d in 
datapoints]
 
-        return {
+        metrics_json = {}
+        for metric, unit in self.__series.metrics.items():
+            metrics_json[metric] = unit.to_json()
+
+        payload = {
             "test_name": self.test_name(),
             "time": self.time(),
             "change_points_timestamp": self.change_points_timestamp,
             "branch_name": self.branch_name(),
             "options": self.options.to_json(),
-            "metrics": self.__series.metrics,
+            "metrics": metrics_json,
             "attributes": self.__series.attributes,
-            "data": self.__series.data,
+            "data": data_json,
             "change_points": change_points_json,
             "weak_change_points": weak_change_points_json,
         }
+        return 
AnalyzedSeriesModel.model_validate(payload).model_dump(mode="python")
 
     @classmethod
     def from_json(cls, analyzed_json):
@@ -445,8 +451,15 @@ class AnalyzedSeries:
 
         new_metrics = {}
 
-        for metric_name, unit in analyzed_json["metrics"].items():
-            new_metrics[metric_name] = Metric(None, None, unit)
+        for metric_name, metric_json in analyzed_json["metrics"].items():
+            if isinstance(metric_json, dict):
+                new_metrics[metric_name] = Metric(
+                    metric_json.get("direction"),
+                    metric_json.get("scale"),
+                    metric_json.get("unit", ""),
+                )
+            else:
+                new_metrics[metric_name] = Metric(None, None, metric_json)
 
         new_series = Series(
             analyzed_json["test_name"],
diff --git a/pyproject.toml b/pyproject.toml
index c243321..d882968 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,7 @@ dependencies = [
     "google-cloud-bigquery>=3.38.0",
     "pg8000>=1.31.5",
     "configargparse>=1.7.1",
+    "pydantic>=2,<3",
 
     # For Python 3.10: last series that supports it
     "scipy>=1.15,<1.16; python_version < '3.11'",
diff --git a/tests/change_point_classes_test.py 
b/tests/change_point_classes_test.py
index 5c30603..5b0e780 100644
--- a/tests/change_point_classes_test.py
+++ b/tests/change_point_classes_test.py
@@ -48,9 +48,7 @@ def make_stats(left=(1.0, 1.0), right=(5.0, 5.0), 
pvalue=0.01):
 
 
 def make_cp(metric="m", index=3, left=(1.0, 1.0), right=(5.0, 5.0), 
pvalue=0.01):
-    return ChangePoint(
-        index=index, qhat=1.0, stats=make_stats(left, right, pvalue), 
metric=metric
-    )
+    return ChangePoint(index=index, qhat=1.0, stats=make_stats(left, right, 
pvalue), metric=metric)
 
 
 def make_group(time, metric="m", index=3, commit="sha"):
@@ -181,20 +179,27 @@ def test_changepoint_copy_is_deep():
     assert cp.stats.mean_1 != -1.0
 
 
-def test_changepoint_serializer_rounded_json():
+def test_changepoint_serializer_json_is_precise():
     cp = make_cp(metric="m", index=3, left=(10.0, 10.0), right=(20.0, 20.0), 
pvalue=0.02)
-    j = ChangePointSerializer(cp).to_json(rounded=True)
+    j = ChangePointSerializer(cp).to_json()
     assert j["metric"] == "m"
     assert j["index"] == 3
-    assert j["forward_change_percent"] == "100"
-    assert all(isinstance(j[k], str) for k in ("magnitude", "mean_before", 
"pvalue"))
+    assert j["qhat"] == 1.0
+    assert j["forward_change_percent"] == 100.0
+    assert j["magnitude"] == 1.0
+    assert j["mean_before"] == 10.0
+    assert j["mean_after"] == 20.0
+    assert j["pvalue"] == 0.02
 
 
-def test_changepoint_serializer_unrounded_json():
+def test_changepoint_json_is_precise():
     cp = make_cp(metric="m", index=3, left=(10.0, 10.0), right=(20.0, 20.0), 
pvalue=0.02)
-    j = ChangePointSerializer(cp).to_json(rounded=False)
+    j = cp.to_json()
+    assert j["metric"] == "m"
     assert j["index"] == 3
+    assert j["qhat"] == 1.0
     assert j["forward_change_percent"] == 100.0
+    assert j["magnitude"] == 1.0
     assert j["mean_before"] == 10.0
     assert j["mean_after"] == 20.0
     assert j["pvalue"] == 0.02
@@ -202,7 +207,7 @@ def test_changepoint_serializer_unrounded_json():
 
 def test_changepoint_to_json_delegates_to_serializer():
     cp = make_cp(metric="m", index=3, left=(10.0, 10.0), right=(20.0, 20.0), 
pvalue=0.02)
-    assert cp.to_json(rounded=False) == 
ChangePointSerializer(cp).to_json(rounded=False)
+    assert cp.to_json() == ChangePointSerializer(cp).to_json()
 
 
 # ChangePointGroup
@@ -251,6 +256,9 @@ def test_group_to_json():
     assert j["attributes"] == {"commit": "abc"}
     assert isinstance(j["changes"], list) and len(j["changes"]) == 1
     assert j["changes"][0]["metric"] == "m"
+    assert j["changes"][0]["qhat"] == 1.0
+    assert j["changes"][0]["forward_change_percent"] == 400.0
+    assert isinstance(j["changes"][0]["mean_before"], float)
 
 
 def test_group_copy_is_deep():
@@ -380,7 +388,11 @@ def test_bytime_append_out_of_order():
 def test_bytime_get_change_points_for_metric_sparse():
     # 'a' appears at t=1 and t=3, 'b' only at t=2 -> sparse columns
     c = ChangePointsByTime.from_list(
-        [make_group(1.0, "a", index=1), make_group(2.0, "b", index=2), 
make_group(3.0, "a", index=3)]
+        [
+            make_group(1.0, "a", index=1),
+            make_group(2.0, "b", index=2),
+            make_group(3.0, "a", index=3),
+        ]
     )
     a_points = c.get_change_points_for_metric("a")
     assert [cp.index for cp in a_points] == [1, 3]
@@ -437,7 +449,9 @@ def test_from_dict_is_metric_keyed_and_returns_by_metric():
 
 
 def test_bymetric_list_constructor():
-    bm = ChangePointsByMetric.from_list([make_group(1.0, "a"), make_group(2.0, 
"a"), make_group(2.0, "b")])
+    bm = ChangePointsByMetric.from_list(
+        [make_group(1.0, "a"), make_group(2.0, "a"), make_group(2.0, "b")]
+    )
     # from_list() returns the class it is called on (asymmetric with from_dict)
     assert isinstance(bm, ChangePointsByMetric)
     assert bm.metrics() == {"a", "b"}
diff --git a/tests/report_test.py b/tests/report_test.py
index 31263b0..d3d9e6f 100644
--- a/tests/report_test.py
+++ b/tests/report_test.py
@@ -70,30 +70,50 @@ def test_report(series, change_points):
 def test_json_report(report):
     output = report.produce_report("test_name_from_config", ReportType.JSON)
     obj = json.loads(output)
-    expected = {'test_name_from_config': [{'attributes': {},
-                                           'changes': 
[{'forward_change_percent': '-11',
-                                                        
'backward_change_percent': '12',
-                                                        'index': 4,
-                                                        'magnitude': 
'0.124108',
-                                                        'mean_after': 
'1.801429',
-                                                        'mean_before': 
'2.025000',
-                                                        'metric': 'series2',
-                                                        'pvalue': '0.000000',
-                                                        'stddev_after': 
'0.026954',
-                                                        'stddev_before': 
'0.011180'}],
-                                          'time': 4},
-                                          {'attributes': {},
-                                          'changes': 
[{'forward_change_percent': '-49',
-                                                       
'backward_change_percent': '98',
-                                                       'index': 6,
-                                                       'magnitude': '0.977513',
-                                                       'mean_after': 
'0.504000',
-                                                       'mean_before': 
'0.996667',
-                                                       'metric': 'series1',
-                                                       'pvalue': '0.000000',
-                                                       'stddev_after': 
'0.025768',
-                                                       'stddev_before': 
'0.067495'}],
-                                           'time': 6}]}
+    first_change = obj["test_name_from_config"][0]["changes"][0]
+    assert first_change["forward_change_percent"] == "-11"
+    assert first_change["backward_change_percent"] == "12"
+    assert isinstance(first_change["mean_before"], str)
+    expected = {
+        "test_name_from_config": [
+            {
+                "attributes": {},
+                "changes": [
+                    {
+                        "forward_change_percent": "-11",
+                        "backward_change_percent": "12",
+                        "index": 4,
+                        "magnitude": "0.124108",
+                        "mean_after": "1.801429",
+                        "mean_before": "2.025000",
+                        "metric": "series2",
+                        "pvalue": "0.000000",
+                        "stddev_after": "0.026954",
+                        "stddev_before": "0.011180",
+                    }
+                ],
+                "time": 4,
+            },
+            {
+                "attributes": {},
+                "changes": [
+                    {
+                        "forward_change_percent": "-49",
+                        "backward_change_percent": "98",
+                        "index": 6,
+                        "magnitude": "0.977513",
+                        "mean_after": "0.504000",
+                        "mean_before": "0.996667",
+                        "metric": "series1",
+                        "pvalue": "0.000000",
+                        "stddev_after": "0.025768",
+                        "stddev_before": "0.067495",
+                    }
+                ],
+                "time": 6,
+            },
+        ]
+    }
     assert isinstance(obj, dict)
     assert obj == expected
 
@@ -104,12 +124,20 @@ def test_regression_only_report(report):
 
 
 def test_simplereport_regression_only():
-    s = Series('t', None, list(range(11)),
-               {'a': Metric(1, 1.0), 'b': Metric(1, 1.0)},
-               {'a': [1.02, 0.95, 0.99, 1.00, 1.12, 0.90, 0.50, 0.51, 0.48, 
0.48, 0.55],
-                'b': [2.02, 2.03, 2.01, 2.04, 1.82, 1.85, 1.79, 1.81, 1.80, 
1.76, 1.78]},
-               {})
-    out = Report(s, s.analyze().change_points_by_time).produce_report('t', 
ReportType.REGRESSIONS_ONLY)
+    s = Series(
+        "t",
+        None,
+        list(range(11)),
+        {"a": Metric(1, 1.0), "b": Metric(1, 1.0)},
+        {
+            "a": [1.02, 0.95, 0.99, 1.00, 1.12, 0.90, 0.50, 0.51, 0.48, 0.48, 
0.55],
+            "b": [2.02, 2.03, 2.01, 2.04, 1.82, 1.85, 1.79, 1.81, 1.80, 1.76, 
1.78],
+        },
+        {},
+    )
+    out = Report(s, s.analyze().change_points_by_time).produce_report(
+        "t", ReportType.REGRESSIONS_ONLY
+    )
     print(out)
     assert "Regressions" in out
     assert "-11.0" in out
diff --git a/tests/series_test.py b/tests/series_test.py
index cab900a..9d48370 100644
--- a/tests/series_test.py
+++ b/tests/series_test.py
@@ -21,6 +21,7 @@ from random import random
 import pytest
 
 from otava.change_point_divisive.base import ChangePointSerializer
+from otava.serialization import AnalyzedSeriesModel
 from otava.series import AnalysisOptions, AnalyzedSeries, Metric, Series
 
 
@@ -268,6 +269,36 @@ def test_analyzed_series_json_round_trip():
     assert restored.to_json()["weak_change_points"] == 
payload["weak_change_points"]
 
 
+def test_analyzed_series_json_uses_pydantic_model_shape():
+    series_1 = [1.02, 0.95, 0.99, 1.00, 1.12, 0.90, 0.50, 0.51, 0.48, 0.48, 
0.55]
+    series_2 = [2.02, 2.03, 2.01, 2.04, 1.82, 1.85, 1.79, 1.81, 1.80, 1.76, 
1.78]
+    time = list(range(len(series_1)))
+    series = Series(
+        "test",
+        branch=None,
+        time=time,
+        metrics={"series1": Metric(1, 1.0), "series2": Metric(1, 1.0)},
+        data={"series1": series_1, "series2": series_2},
+        attributes={},
+    )
+
+    analyzed_series = series.analyze()
+    analyzed_series.append(
+        time=[len(time)], new_data={"series1": [0.5], "series2": [1.97]}, 
attributes={}
+    )
+
+    payload = analyzed_series.to_json()
+    model = AnalyzedSeriesModel.model_validate(payload)
+    dumped = model.model_dump(mode="python")
+
+    change = dumped["change_points"]["series2"][0]["changes"][0]
+    weak_change = dumped["weak_change_points"]["series2"][1]["changes"][0]
+    assert isinstance(change["mean_before"], float)
+    assert isinstance(change["forward_change_percent"], float)
+    assert isinstance(weak_change["pvalue"], float)
+    assert AnalyzedSeries.from_json(dumped).to_json()["change_points"] == 
dumped["change_points"]
+
+
 def test_validate():
     series_1 = [1.02, 0.95, 0.99, 1.00, 1.12, 0.90, 0.50, 0.51, 0.48, 0.48, 
0.55]
     series_2 = [2.02, 2.03, 2.01, 2.04, 1.82, 1.85, 1.79, 1.81, 1.80, 1.76, 
1.78]
diff --git a/uv.lock b/uv.lock
index 8295185..3ea5f6d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -8,6 +8,15 @@ resolution-markers = [
     "python_full_version < '3.11'",
 ]
 
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz";,
 hash = 
"sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size 
= 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl";,
 hash = 
"sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size 
= 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
 [[package]]
 name = "apache-otava"
 version = "0.8.0"
@@ -19,6 +28,7 @@ dependencies = [
     { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
     { name = "numpy", version = "2.3.5", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
     { name = "pg8000" },
+    { name = "pydantic" },
     { name = "pystache" },
     { name = "python-dateutil" },
     { name = "requests" },
@@ -60,6 +70,7 @@ requires-dist = [
     { name = "numpy", marker = "python_full_version >= '3.14'", specifier = 
">=2.3.2,<2.4" },
     { name = "pg8000", specifier = ">=1.31.5" },
     { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.5.0" },
+    { name = "pydantic", specifier = ">=2,<3" },
     { name = "pystache", specifier = ">=0.6.8" },
     { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.1" },
     { name = "pytest-benchmark", marker = "extra == 'dev'", specifier = 
">=5.2.3" },
@@ -950,6 +961,137 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl";,
 hash = 
"sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size 
= 31594, upload-time = "2025-06-20T18:49:47.491Z" },
 ]
 
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "annotated-types" },
+    { name = "pydantic-core" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz";,
 hash = 
"sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size 
= 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl";,
 hash = 
"sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size 
= 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz";,
 hash = 
"sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size 
= 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size 
= 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
+    { url = 
"https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl";,
 hash = 
"sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size 
= 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size 
= 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
+    { url = 
"https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size 
= 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
+    { url = 
"https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size 
= 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
+    { url = 
"https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size 
= 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size 
= 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
+    { url = 
"https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size 
= 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
+    { url = 
"https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size 
= 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
+    { url = 
"https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size 
= 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
+    { url = 
"https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size 
= 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size 
= 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
+    { url = 
"https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl";,
 hash = 
"sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size 
= 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
+    { url = 
"https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl";,
 hash = 
"sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size 
= 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size 
= 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl";,
 hash = 
"sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size 
= 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+    { url = 
"https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size 
= 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size 
= 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+    { url = 
"https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size 
= 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+    { url = 
"https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size 
= 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+    { url = 
"https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size 
= 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size 
= 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+    { url = 
"https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size 
= 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size 
= 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+    { url = 
"https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size 
= 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size 
= 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl";,
 hash = 
"sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size 
= 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+    { url = 
"https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl";,
 hash = 
"sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size 
= 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+    { url = 
"https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl";,
 hash = 
"sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size 
= 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size 
= 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+    { url = 
"https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl";,
 hash = 
"sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size 
= 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size 
= 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size 
= 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size 
= 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+    { url = 
"https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size 
= 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size 
= 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+    { url = 
"https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size 
= 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+    { url = 
"https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size 
= 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size 
= 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size 
= 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size 
= 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+    { url = 
"https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl";,
 hash = 
"sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size 
= 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+    { url = 
"https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl";,
 hash = 
"sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size 
= 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl";,
 hash = 
"sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size 
= 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+    { url = 
"https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size 
= 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl";,
 hash = 
"sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size 
= 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+    { url = 
"https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size 
= 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+    { url = 
"https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size 
= 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+    { url = 
"https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size 
= 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size 
= 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+    { url = 
"https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size 
= 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+    { url = 
"https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size 
= 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size 
= 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size 
= 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+    { url = 
"https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size 
= 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size 
= 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl";,
 hash = 
"sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size 
= 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl";,
 hash = 
"sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size 
= 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl";,
 hash = 
"sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size 
= 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size 
= 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl";,
 hash = 
"sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size 
= 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+    { url = 
"https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size 
= 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+    { url = 
"https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size 
= 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+    { url = 
"https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size 
= 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+    { url = 
"https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size 
= 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+    { url = 
"https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size 
= 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+    { url = 
"https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size 
= 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+    { url = 
"https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size 
= 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size 
= 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+    { url = 
"https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size 
= 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+    { url = 
"https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size 
= 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl";,
 hash = 
"sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size 
= 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl";,
 hash = 
"sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size 
= 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+    { url = 
"https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl";,
 hash = 
"sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size 
= 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size 
= 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl";,
 hash = 
"sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size 
= 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size 
= 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+    { url = 
"https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size 
= 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size 
= 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+    { url = 
"https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size 
= 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size 
= 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+    { url = 
"https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size 
= 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+    { url = 
"https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size 
= 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+    { url = 
"https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size 
= 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size 
= 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size 
= 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+    { url = 
"https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl";,
 hash = 
"sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size 
= 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+    { url = 
"https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl";,
 hash = 
"sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size 
= 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl";,
 hash = 
"sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size 
= 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size 
= 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl";,
 hash = 
"sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size 
= 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+    { url = 
"https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size 
= 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+    { url = 
"https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size 
= 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+    { url = 
"https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size 
= 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl";,
 hash = 
"sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size 
= 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size 
= 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size 
= 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+    { url = 
"https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size 
= 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+    { url = 
"https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl";,
 hash = 
"sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size 
= 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+    { url = 
"https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size 
= 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+    { url = 
"https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl";,
 hash = 
"sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size 
= 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+    { url = 
"https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size 
= 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size 
= 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+    { url = 
"https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size 
= 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+    { url = 
"https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl";,
 hash = 
"sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size 
= 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
 [[package]]
 name = "pyflakes"
 version = "3.4.0"
@@ -1601,6 +1743,18 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl";,
 hash = 
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size 
= 44614, upload-time = "2025-08-25T13:49:24.86Z" },
 ]
 
+[[package]]
+name = "typing-inspection"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz";,
 hash = 
"sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size 
= 76928, upload-time = "2026-08-12T12:37:25.997Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl";,
 hash = 
"sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size 
= 14750, upload-time = "2026-08-12T12:37:24.648Z" },
+]
+
 [[package]]
 name = "tzdata"
 version = "2025.2"

Reply via email to