Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.

Commit 8e222db

Browse files
committed
Replace OpenAPI models with slim Pydantic models
- Create new gsrest/models/ with ~700 lines of Pydantic v2 models - Delete openapi_server/models/ (~12,000 lines, 56 files) - Delete openapi_server/ utilities (typing_utils.py, util.py) - Simplify translators.py with model_validate pattern - Update imports across services, routes, and tests - Fix Concept model optional fields (uri, description) All 66 unit tests pass, 182 migration tests pass.
1 parent 36a6401 commit 8e222db

88 files changed

Lines changed: 1277 additions & 12884 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

gsrest/builtin/plugins/obfuscate_tags/obfuscate_tags.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,24 @@
88
obfuscate_tag_if_not_public,
99
)
1010

11+
from gsrest.models import (
12+
AddressTags,
13+
Entity,
14+
NeighborEntities,
15+
SearchResultLeaf,
16+
SearchResultLevel1,
17+
SearchResultLevel2,
18+
SearchResultLevel3,
19+
SearchResultLevel4,
20+
SearchResultLevel5,
21+
SearchResultLevel6,
22+
)
1123
from gsrest.plugins import (
1224
Plugin,
1325
get_request_header,
1426
get_request_path,
1527
get_request_query_string,
1628
)
17-
from openapi_server.models.address_tags import AddressTags
18-
from openapi_server.models.entity import Entity
19-
from openapi_server.models.neighbor_entities import NeighborEntities
20-
from openapi_server.models.search_result_leaf import SearchResultLeaf
21-
from openapi_server.models.search_result_level1 import SearchResultLevel1
22-
from openapi_server.models.search_result_level2 import SearchResultLevel2
23-
from openapi_server.models.search_result_level3 import SearchResultLevel3
24-
from openapi_server.models.search_result_level4 import SearchResultLevel4
25-
from openapi_server.models.search_result_level5 import SearchResultLevel5
26-
from openapi_server.models.search_result_level6 import SearchResultLevel6
2729

2830
GROUPS_HEADER_NAME = "X-Consumer-Groups"
2931
NO_OBFUSCATION_MARKER_PATTERN = re.compile(r"(private|tags-private)")

gsrest/models/__init__.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Slim Pydantic models for the GraphSense REST API.
2+
3+
This module provides the API response models as native Pydantic v2 models,
4+
replacing the generated OpenAPI models with a clean, minimal implementation.
5+
"""
6+
7+
from gsrest.models.addresses import Address, NeighborAddress, NeighborAddresses
8+
from gsrest.models.base import APIModel
9+
from gsrest.models.blocks import Block, BlockAtDate
10+
from gsrest.models.common import LabeledItemRef
11+
from gsrest.models.entities import (
12+
Entity,
13+
EntityAddresses,
14+
NeighborEntities,
15+
NeighborEntity,
16+
)
17+
from gsrest.models.general import (
18+
Actor,
19+
ActorContext,
20+
Concept,
21+
CurrencyStats,
22+
ExternalConversion,
23+
Rates,
24+
RelatedAddress,
25+
RelatedAddresses,
26+
Stats,
27+
Taxonomy,
28+
TokenConfig,
29+
TokenConfigs,
30+
)
31+
from gsrest.models.search import (
32+
SearchResult,
33+
SearchResultByCurrency,
34+
SearchResultLeaf,
35+
SearchResultLevel1,
36+
SearchResultLevel2,
37+
SearchResultLevel3,
38+
SearchResultLevel4,
39+
SearchResultLevel5,
40+
SearchResultLevel6,
41+
)
42+
from gsrest.models.tags import (
43+
AddressTag,
44+
AddressTags,
45+
LabelSummary,
46+
Tag,
47+
TagCloudEntry,
48+
TagSummary,
49+
UserTagReportResponse,
50+
)
51+
from gsrest.models.transactions import (
52+
AddressTxs,
53+
AddressTxUtxo,
54+
Link,
55+
LinkUtxo,
56+
Links,
57+
Tx,
58+
TxAccount,
59+
TxRef,
60+
Txs,
61+
TxSummary,
62+
TxUtxo,
63+
TxValue,
64+
)
65+
from gsrest.models.values import Rate, Values
66+
67+
__all__ = [
68+
# Base
69+
"APIModel",
70+
# Common
71+
"LabeledItemRef",
72+
# Values
73+
"Rate",
74+
"Values",
75+
# Transactions
76+
"TxSummary",
77+
"TxRef",
78+
"TxValue",
79+
"TxUtxo",
80+
"TxAccount",
81+
"Tx",
82+
"Txs",
83+
"AddressTxUtxo",
84+
"AddressTxs",
85+
"LinkUtxo",
86+
"Link",
87+
"Links",
88+
# Tags
89+
"Tag",
90+
"AddressTag",
91+
"AddressTags",
92+
"TagCloudEntry",
93+
"LabelSummary",
94+
"TagSummary",
95+
"UserTagReportResponse",
96+
# Addresses
97+
"Address",
98+
"NeighborAddress",
99+
"NeighborAddresses",
100+
# Entities
101+
"Entity",
102+
"NeighborEntity",
103+
"NeighborEntities",
104+
"EntityAddresses",
105+
# Blocks
106+
"Block",
107+
"BlockAtDate",
108+
# Search
109+
"SearchResultByCurrency",
110+
"SearchResult",
111+
"SearchResultLeaf",
112+
"SearchResultLevel1",
113+
"SearchResultLevel2",
114+
"SearchResultLevel3",
115+
"SearchResultLevel4",
116+
"SearchResultLevel5",
117+
"SearchResultLevel6",
118+
# General
119+
"CurrencyStats",
120+
"Stats",
121+
"Rates",
122+
"Taxonomy",
123+
"Concept",
124+
"ActorContext",
125+
"Actor",
126+
"TokenConfig",
127+
"TokenConfigs",
128+
"RelatedAddress",
129+
"RelatedAddresses",
130+
"ExternalConversion",
131+
]

gsrest/models/addresses.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Address-related API models."""
2+
3+
from typing import Any, Optional
4+
5+
from pydantic import ConfigDict
6+
7+
from gsrest.models.base import APIModel
8+
from gsrest.models.common import LabeledItemRef
9+
from gsrest.models.transactions import TxSummary
10+
from gsrest.models.values import Values
11+
12+
13+
class Address(APIModel):
14+
"""Address model."""
15+
16+
# Allow extra fields for test fixtures that set .tags
17+
model_config = ConfigDict(
18+
populate_by_name=True,
19+
from_attributes=True,
20+
extra="allow",
21+
)
22+
23+
currency: str
24+
address: str
25+
entity: int
26+
balance: Values
27+
total_received: Values
28+
total_spent: Values
29+
first_tx: TxSummary
30+
last_tx: TxSummary
31+
in_degree: int
32+
out_degree: int
33+
no_incoming_txs: int
34+
no_outgoing_txs: int
35+
token_balances: Optional[dict[str, Values]] = None
36+
total_tokens_received: Optional[dict[str, Values]] = None
37+
total_tokens_spent: Optional[dict[str, Values]] = None
38+
actors: Optional[list[LabeledItemRef]] = None
39+
is_contract: Optional[bool] = None
40+
status: Optional[str] = None
41+
42+
def to_dict(self, shallow: bool = False) -> dict[str, Any]:
43+
"""Override to exclude extra fields (like 'tags') from serialization."""
44+
result = super().to_dict(shallow=shallow)
45+
# Remove any extra fields that aren't part of the API response
46+
result.pop("tags", None)
47+
return result
48+
49+
50+
class NeighborAddress(APIModel):
51+
"""Neighbor address model."""
52+
53+
value: Values
54+
no_txs: int
55+
address: Address
56+
labels: Optional[list[str]] = None
57+
token_values: Optional[dict[str, Values]] = None
58+
59+
60+
class NeighborAddresses(APIModel):
61+
"""Paginated list of neighbor addresses."""
62+
63+
neighbors: list[NeighborAddress]
64+
next_page: Optional[str] = None

gsrest/models/base.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Base configuration for API models."""
2+
3+
from typing import TypeVar
4+
5+
from pydantic import BaseModel, ConfigDict
6+
7+
T = TypeVar("T", bound="APIModel")
8+
9+
10+
class APIModel(BaseModel):
11+
"""Base class for all API models with shared configuration."""
12+
13+
model_config = ConfigDict(
14+
populate_by_name=True,
15+
from_attributes=True,
16+
)
17+
18+
@classmethod
19+
def from_dict(cls: type[T], dikt: dict) -> T:
20+
"""Create model from dict (backward compatible with old OpenAPI models)."""
21+
return cls.model_validate(dikt)
22+
23+
def to_dict(self, shallow: bool = False) -> dict:
24+
"""Convert model to dict (backward compatible with old OpenAPI models).
25+
26+
Args:
27+
shallow: If True, return raw values without recursively converting
28+
nested models to dicts. This is used by bulk CSV flattening.
29+
ALL fields are included (even None) to ensure consistent CSV columns.
30+
31+
Returns:
32+
Dictionary representation of the model.
33+
"""
34+
if shallow:
35+
# Return raw values for bulk flattening - include ALL fields for CSV columns
36+
return {key: value for key, value in self.__dict__.items()}
37+
38+
# Normal case: recursively convert to dict, excluding None
39+
return self.model_dump(exclude_none=True)

gsrest/models/blocks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Block-related API models."""
2+
3+
from typing import Optional
4+
5+
from gsrest.models.base import APIModel
6+
7+
8+
class Block(APIModel):
9+
"""Block model."""
10+
11+
block_hash: str
12+
currency: str
13+
height: int
14+
no_txs: int
15+
timestamp: int
16+
17+
18+
class BlockAtDate(APIModel):
19+
"""Block at date model."""
20+
21+
before_block: Optional[int] = None
22+
before_timestamp: Optional[int] = None
23+
after_block: Optional[int] = None
24+
after_timestamp: Optional[int] = None

gsrest/models/common.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Common API models shared across domains."""
2+
3+
from gsrest.models.base import APIModel
4+
5+
6+
class LabeledItemRef(APIModel):
7+
"""Reference to a labeled item."""
8+
9+
id: str
10+
label: str

gsrest/models/entities.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Entity-related API models."""
2+
3+
from typing import Optional, Union
4+
5+
from gsrest.models.addresses import Address
6+
from gsrest.models.base import APIModel
7+
from gsrest.models.common import LabeledItemRef
8+
from gsrest.models.tags import AddressTag
9+
from gsrest.models.transactions import TxSummary
10+
from gsrest.models.values import Values
11+
12+
13+
class Entity(APIModel):
14+
"""Entity model."""
15+
16+
currency: str
17+
entity: int
18+
root_address: str
19+
balance: Values
20+
total_received: Values
21+
total_spent: Values
22+
first_tx: TxSummary
23+
last_tx: TxSummary
24+
in_degree: int
25+
out_degree: int
26+
no_addresses: int
27+
no_incoming_txs: int
28+
no_outgoing_txs: int
29+
no_address_tags: int
30+
token_balances: Optional[dict[str, Values]] = None
31+
total_tokens_received: Optional[dict[str, Values]] = None
32+
total_tokens_spent: Optional[dict[str, Values]] = None
33+
actors: Optional[list[LabeledItemRef]] = None
34+
best_address_tag: Optional[AddressTag] = None
35+
36+
37+
class NeighborEntity(APIModel):
38+
"""Neighbor entity model."""
39+
40+
value: Values
41+
no_txs: int
42+
entity: Optional[Union[Entity, int]] = None
43+
labels: Optional[list[str]] = None
44+
token_values: Optional[dict[str, Values]] = None
45+
46+
47+
class NeighborEntities(APIModel):
48+
"""Paginated list of neighbor entities."""
49+
50+
neighbors: list[NeighborEntity]
51+
next_page: Optional[str] = None
52+
53+
54+
class EntityAddresses(APIModel):
55+
"""Paginated list of addresses in an entity."""
56+
57+
addresses: list[Address]
58+
next_page: Optional[str] = None

0 commit comments

Comments
 (0)