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

Commit c0d663a

Browse files
committed
Add API key auth to fastapi and change REST URL to iknaio.com
1 parent 71193d2 commit c0d663a

15 files changed

Lines changed: 167 additions & 183 deletions

File tree

clients/python/Makefile

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,13 @@ GENERATOR_VERSION ?= v7.19.0
55
TEMPLATES_DIR ?= ../python-templates
66
COMPAT_DIR ?= ../python-compat
77

8-
# Legacy v5.2.1 generator (for comparison/rollback)
9-
generate-openapi-client-v5:
10-
@echo "Downloading openapi.json from $(OPENAPI_URL)..."
11-
curl -s $(OPENAPI_URL) > openapi.json.tmp
12-
@echo "Modifying server URL to production..."
13-
python scripts/openapi_spec_add_auth.py openapi.json.tmp https://api.ikna.io > openapi.json.modified
14-
@echo "Generating client with v5.2.1..."
15-
docker run --rm \
16-
--user $(shell id -u):$(shell id -g) \
17-
-v "${PWD}:/build:Z" \
18-
-v "${PWD}/$(TEMPLATES_DIR):/templates:Z" \
19-
openapitools/openapi-generator-cli:v5.2.1 \
20-
generate -i /build/openapi.json.modified \
21-
-g python \
22-
-t /templates \
23-
-o /build \
24-
--additional-properties=packageName=graphsense \
25-
--additional-properties=projectName=graphsense-python
26-
@echo "Cleaning up temporary files..."
27-
rm -f openapi.json.tmp openapi.json.modified
288

299
# v7 generator with backward compatibility patches
3010
generate-openapi-client:
3111
@echo "Downloading openapi.json from $(OPENAPI_URL)..."
3212
curl -s $(OPENAPI_URL) > openapi.json.tmp
3313
@echo "Modifying server URL to production..."
34-
python scripts/openapi_spec_add_auth.py openapi.json.tmp https://api.ikna.io > openapi.json.modified
14+
jq '.servers[0].url = "https://api.iknaio.com"' openapi.json.tmp > openapi.json.modified
3515
@API_VERSION=$$(jq -r '.info.version' openapi.json.tmp) && \
3616
echo "Generating client with $(GENERATOR_VERSION) (API version: $$API_VERSION)..." && \
3717
docker run --rm \

clients/python/graphsense/models/address.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ class Address(BaseModel):
4747
is_contract: Optional[StrictBool] = None
4848
status: Optional[StrictStr] = None
4949
__properties: ClassVar[List[str]] = ["currency", "address", "entity", "balance", "total_received", "total_spent", "first_tx", "last_tx", "in_degree", "out_degree", "no_incoming_txs", "no_outgoing_txs", "token_balances", "total_tokens_received", "total_tokens_spent", "actors", "is_contract", "status"]
50-
@field_validator('actors', mode='wrap')
50+
@field_validator('actors', mode='before')
5151
@classmethod
52-
def wrap_actors_compat(cls, v, handler):
52+
def wrap_actors_compat(cls, v):
5353
"""Wrap actors in CompatList for backward compatibility."""
54-
validated = handler(v)
55-
if validated is not None and not isinstance(validated, CompatList):
56-
return CompatList(validated) if isinstance(validated, list) else validated
57-
return validated
58-
54+
if v is not None and not isinstance(v, CompatList):
55+
return CompatList(v) if isinstance(v, list) else v
56+
return v
5957

6058

6159
model_config = ConfigDict(

clients/python/graphsense/models/address_tx_utxo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def wrap_height_compat(cls, v, handler):
4444
return validated
4545

4646

47-
4847
model_config = ConfigDict(
4948
populate_by_name=True,
5049
validate_assignment=True,

clients/python/graphsense/models/block.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def wrap_height_compat(cls, v, handler):
4141
return validated
4242

4343

44-
4544
model_config = ConfigDict(
4645
populate_by_name=True,
4746
validate_assignment=True,

clients/python/graphsense/models/entity.py

Lines changed: 72 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,75 @@ def actual_instance_must_validate_anyof(cls, v):
6060

6161
instance = Entity.model_construct()
6262
error_messages = []
63-
# validate data type: Entity (or dict that can become Entity)
64-
if isinstance(v, Entity):
65-
return v
66-
67-
# Check if it's a dict (Entity-like object)
68-
if isinstance(v, dict):
63+
# validate data type: Entity
64+
if not isinstance(v, Entity):
65+
error_messages.append(f"Error! Input type `{type(v)}` is not `Entity`")
66+
else:
6967
return v
7068

7169
# validate data type: int
72-
if isinstance(v, int):
73-
try:
74-
instance.anyof_schema_2_validator = v
75-
return v
76-
except (ValidationError, ValueError) as e:
77-
error_messages.append(str(e))
78-
else:
79-
error_messages.append(f"Error! Input type `{type(v)}` is not `Entity`, `dict`, or `int`")
80-
70+
try:
71+
instance.anyof_schema_2_validator = v
72+
return v
73+
except (ValidationError, ValueError) as e:
74+
error_messages.append(str(e))
8175
if error_messages:
8276
# no match
8377
raise ValueError("No match found when setting the actual_instance in Entity with anyOf schemas: Entity, int. Details: " + ", ".join(error_messages))
8478
else:
8579
return v
80+
def __getattr__(self, name: str):
81+
"""Delegate attribute access to actual_instance for backward compatibility.
82+
83+
This allows code like `tx.height` instead of `tx.actual_instance.height`.
84+
"""
85+
if name.startswith('_') or name in (
86+
'actual_instance', 'one_of_schemas', 'model_config',
87+
'discriminator_value_class_map', 'model_fields', 'model_computed_fields',
88+
'model_extra', 'model_fields_set', 'oneof_schema_1_validator',
89+
'oneof_schema_2_validator', 'oneof_schema_3_validator'
90+
):
91+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
92+
93+
actual = object.__getattribute__(self, 'actual_instance')
94+
if actual is None:
95+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
96+
return getattr(actual, name)
97+
98+
def __setattr__(self, name: str, value):
99+
"""Delegate attribute setting to actual_instance for backward compatibility."""
100+
if name.startswith('_') or name in (
101+
'actual_instance', 'one_of_schemas', 'model_config',
102+
'discriminator_value_class_map', 'model_fields', 'model_computed_fields',
103+
'model_extra', 'model_fields_set'
104+
):
105+
super().__setattr__(name, value)
106+
return
107+
108+
try:
109+
actual = object.__getattribute__(self, 'actual_instance')
110+
if actual is not None:
111+
setattr(actual, name, value)
112+
return
113+
except AttributeError:
114+
pass
115+
116+
super().__setattr__(name, value)
117+
118+
def __getitem__(self, key):
119+
"""Delegate subscript access to actual_instance for backward compatibility.
120+
121+
This allows code like `tx['height']` instead of `tx.actual_instance['height']`.
122+
"""
123+
actual = object.__getattribute__(self, 'actual_instance')
124+
if actual is None:
125+
raise KeyError(key)
126+
# Try dict-style access first, then attribute access
127+
if hasattr(actual, '__getitem__'):
128+
return actual[key]
129+
return getattr(actual, key)
130+
131+
86132

87133
@classmethod
88134
def from_dict(cls, obj: Dict[str, Any]) -> Self:
@@ -96,28 +142,21 @@ def from_json(cls, json_str: str) -> Self:
96142
return instance
97143

98144
error_messages = []
99-
# Try to deserialize as int first (simpler case)
145+
# anyof_schema_1_validator: Optional[Entity] = None
100146
try:
101-
# validation
102-
parsed_data = json.loads(json_str)
103-
if isinstance(parsed_data, int):
104-
instance.anyof_schema_2_validator = parsed_data
105-
# assign value to actual_instance
106-
instance.actual_instance = instance.anyof_schema_2_validator
107-
return instance
147+
instance.actual_instance = Entity.from_json(json_str)
148+
return instance
108149
except (ValidationError, ValueError) as e:
109-
error_messages.append(str(e))
110-
111-
# If not an int, try to deserialize as Entity object (dict)
150+
error_messages.append(str(e))
151+
# deserialize data into int
112152
try:
113-
parsed_data = json.loads(json_str)
114-
if isinstance(parsed_data, dict):
115-
# For Entity objects, just use the dict directly as actual_instance
116-
# This avoids the infinite recursion
117-
instance.actual_instance = parsed_data
118-
return instance
153+
# validation
154+
instance.anyof_schema_2_validator = json.loads(json_str)
155+
# assign value to actual_instance
156+
instance.actual_instance = instance.anyof_schema_2_validator
157+
return instance
119158
except (ValidationError, ValueError) as e:
120-
error_messages.append(str(e))
159+
error_messages.append(str(e))
121160

122161
if error_messages:
123162
# no match
@@ -149,46 +188,6 @@ def to_str(self) -> str:
149188
"""Returns the string representation of the actual instance"""
150189
return pprint.pformat(self.model_dump())
151190

152-
def __getitem__(self, key):
153-
"""Allow dict-style access to entity fields for backward compatibility."""
154-
if self.actual_instance is None:
155-
raise KeyError(key)
156-
157-
if isinstance(self.actual_instance, dict):
158-
return self.actual_instance[key]
159-
elif isinstance(self.actual_instance, int):
160-
raise KeyError(f"Cannot access key '{key}' on int entity")
161-
else:
162-
# For Entity instances
163-
return getattr(self.actual_instance, key)
164-
165-
def __getattr__(self, name):
166-
"""Allow attribute access to entity fields for backward compatibility."""
167-
# Avoid infinite recursion for private attributes and Pydantic internals
168-
if name.startswith('_') or name in ('actual_instance', 'anyof_schema_1_validator', 'anyof_schema_2_validator'):
169-
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
170-
171-
actual = object.__getattribute__(self, 'actual_instance')
172-
if actual is None:
173-
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
174-
175-
if isinstance(actual, dict):
176-
if name in actual:
177-
from graphsense.compat import DictModel
178-
value = actual[name]
179-
# Wrap nested dicts in DictModel for attribute access
180-
if isinstance(value, dict):
181-
return DictModel(value)
182-
elif isinstance(value, list):
183-
return [DictModel(item) if isinstance(item, dict) else item for item in value]
184-
return value
185-
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
186-
elif isinstance(actual, int):
187-
raise AttributeError(f"Cannot access attribute '{name}' on int entity")
188-
else:
189-
# For Entity instances
190-
return getattr(actual, name)
191-
192191
# TODO: Rewrite to not use raise_errors
193192
Entity.model_rebuild(raise_errors=False)
194193

clients/python/graphsense/models/link_utxo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def wrap_height_compat(cls, v, handler):
4444
return validated
4545

4646

47-
4847
model_config = ConfigDict(
4948
populate_by_name=True,
5049
validate_assignment=True,

clients/python/graphsense/models/rates.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Rates(BaseModel):
2727
Exchange rates model.
2828
""" # noqa: E501
2929
rates: Optional[List[Rate]] = None
30-
height: Optional[StrictInt] = None
30+
height: Optional[int] = None
3131
__properties: ClassVar[List[str]] = ["rates", "height"]
3232
@field_validator('height', mode='wrap')
3333
@classmethod
@@ -39,7 +39,6 @@ def wrap_height_compat(cls, v, handler):
3939
return validated
4040

4141

42-
4342
model_config = ConfigDict(
4443
populate_by_name=True,
4544
validate_assignment=True,

clients/python/graphsense/models/search_result.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ class SearchResult(BaseModel):
3131
labels: List[StrictStr]
3232
actors: Optional[List[LabeledItemRef]] = None
3333
__properties: ClassVar[List[str]] = ["currencies", "labels", "actors"]
34-
@field_validator('actors', mode='wrap')
34+
@field_validator('actors', mode='before')
3535
@classmethod
36-
def wrap_actors_compat(cls, v, handler):
36+
def wrap_actors_compat(cls, v):
3737
"""Wrap actors in CompatList for backward compatibility."""
38-
validated = handler(v)
39-
if validated is not None and not isinstance(validated, CompatList):
40-
return CompatList(validated) if isinstance(validated, list) else validated
41-
return validated
42-
38+
if v is not None and not isinstance(v, CompatList):
39+
return CompatList(v) if isinstance(v, list) else v
40+
return v
4341

4442

4543
model_config = ConfigDict(

clients/python/graphsense/models/tx_account.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ def wrap_height_compat(cls, v, handler):
5151
return validated
5252

5353

54-
5554
model_config = ConfigDict(
5655
populate_by_name=True,
5756
validate_assignment=True,

clients/python/graphsense/models/tx_summary.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def wrap_height_compat(cls, v, handler):
3939
return validated
4040

4141

42-
4342
model_config = ConfigDict(
4443
populate_by_name=True,
4544
validate_assignment=True,

0 commit comments

Comments
 (0)