Skip to content

Commit 0c36ebe

Browse files
committed
feat: add pydantic API models and validation layer
1 parent 9279636 commit 0c36ebe

5 files changed

Lines changed: 748 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ dynamic = ["version"]
1313
dependencies = [
1414
"click",
1515
"mutagen",
16+
"pydantic>=2,<3; python_version >= '3.8'",
17+
"pydantic>=1.8.2,<1.9; python_version < '3.8'",
1618
"pathvalidate",
1719
"requests",
1820
"tabulate",
1921
"wcwidth",
2022
]
21-
requires-python = ">=3.6"
23+
requires-python = ">=3.6.1"
2224

2325
[project.optional-dependencies]
2426
dev = [

tests/test_models.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pydantic import ValidationError
2+
import pytest
3+
4+
from vistopia.models import (
5+
Article,
6+
Catalog,
7+
CatalogPart,
8+
ContentShow,
9+
SearchPage,
10+
SearchItem,
11+
SearchResult,
12+
SubscriptionsPage,
13+
SubscriptionItem,
14+
SubscriptionsList,
15+
validate_model,
16+
)
17+
18+
19+
def _field_names(model_cls):
20+
if hasattr(model_cls, "model_fields"):
21+
return set(model_cls.model_fields.keys())
22+
return set(model_cls.__fields__.keys())
23+
24+
25+
def test_models_cover_all_keys_used_by_runtime_code():
26+
# Keys referenced in vistopia/main.py and vistopia/visitor.py.
27+
assert {"author", "title", "type", "background_img", "catalog"} <= _field_names(Catalog)
28+
assert {"catalog_number", "catalog_title", "part"} <= _field_names(CatalogPart)
29+
assert {"sort_number", "title", "duration_str", "media_key_full_url", "content_url", "article_id"} <= _field_names(Article)
30+
assert {"author", "title"} <= _field_names(ContentShow)
31+
assert {"data"} <= _field_names(SubscriptionsList)
32+
assert {"content_id", "title", "subtitle"} <= _field_names(SubscriptionItem)
33+
assert {"data"} <= _field_names(SearchResult)
34+
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SearchPage)
35+
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SubscriptionsPage)
36+
assert {"id", "author", "title", "share_desc", "data_type", "subtitle"} <= _field_names(SearchItem)
37+
38+
39+
def test_catalog_model_parses_nested_structure():
40+
payload = {
41+
"author": "Author A",
42+
"title": "Show A",
43+
"type": "charge",
44+
"catalog": [
45+
{
46+
"catalog_number": "01",
47+
"catalog_title": "Part One",
48+
"part": [
49+
{
50+
"sort_number": "00",
51+
"title": "Episode 1",
52+
"duration_str": "1:00",
53+
"media_key_full_url": "https://example.com/a.mp3",
54+
"content_url": "https://example.com/a",
55+
"article_id": "100",
56+
}
57+
],
58+
}
59+
],
60+
}
61+
62+
model = validate_model(Catalog, payload)
63+
assert model.title == "Show A"
64+
assert model.catalog[0].catalog_number == "01"
65+
assert model.catalog[0].part[0].sort_number == "00"
66+
67+
68+
def test_search_result_coerces_numeric_id_from_string():
69+
payload = {
70+
"data": {
71+
"current_page": 1,
72+
"from": 1,
73+
"last_page": 1,
74+
"next_page_url": None,
75+
"per_page": 20,
76+
"prev_page_url": None,
77+
"to": 1,
78+
"total": 1,
79+
"data": [
80+
{
81+
"id": "11",
82+
"author": "梁文道",
83+
"title": "八分",
84+
"share_desc": "知识只求八分饱",
85+
"data_type": "content",
86+
"subtitle": "",
87+
}
88+
],
89+
}
90+
}
91+
92+
model = validate_model(SearchResult, payload)
93+
assert model.data.data[0].id == 11
94+
assert model.data.from_ == 1
95+
96+
97+
def test_catalog_model_raises_on_missing_required_fields():
98+
with pytest.raises(ValidationError):
99+
validate_model(Catalog, {"title": "missing-author-and-catalog"})

0 commit comments

Comments
 (0)