Skip to content

Commit 602b03b

Browse files
authored
Merge pull request #41 from VaitaR/dev
Dev
2 parents 4e64bb9 + ec3c94c commit 602b03b

52 files changed

Lines changed: 5592 additions & 6715 deletions

Some content is hidden

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

aiochainscan/__init__.py

Lines changed: 354 additions & 2915 deletions
Large diffs are not rendered by default.

aiochainscan/_facade.py

Lines changed: 2901 additions & 0 deletions
Large diffs are not rendered by default.

aiochainscan/adapters/orjson_parser.py

Lines changed: 0 additions & 112 deletions
This file was deleted.

aiochainscan/adapters/simple_provider_federator.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,3 @@ def should_use_graphql(
2020
if preferred is True:
2121
return True
2222
return supported
23-
24-
# naive in-memory health map; per-process only
25-
_failures: dict[tuple[str, str, str], int] = {}
26-
27-
def report_success(self, feature: str, *, api_kind: str, network: str) -> None:
28-
key = (feature, api_kind, network)
29-
self._failures.pop(key, None)
30-
31-
def report_failure(self, feature: str, *, api_kind: str, network: str) -> None:
32-
key = (feature, api_kind, network)
33-
self._failures[key] = self._failures.get(key, 0) + 1
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Awaitable, Callable, Mapping
4+
from time import monotonic
5+
from typing import Any, TypeVar
6+
7+
from aiochainscan.core.context import ProviderContext
8+
from aiochainscan.domain.models import Address, TxHash
9+
10+
T = TypeVar('T')
11+
12+
13+
class SmartDataProvider:
14+
"""Transport router for GraphQL-vs-REST data retrieval.
15+
16+
Services should delegate transport decisions here and keep business logic
17+
transport-agnostic.
18+
"""
19+
20+
def __init__(self, ctx: ProviderContext) -> None:
21+
self._ctx = ctx
22+
23+
def _can_use_graphql(self, feature: str) -> bool:
24+
return (
25+
self._ctx.federator is not None
26+
and self._ctx.gql is not None
27+
and self._ctx.gql_builder is not None
28+
and self._ctx.federator.should_use_graphql(
29+
feature, api_kind=self._ctx.api_kind, network=self._ctx.network
30+
)
31+
)
32+
33+
def _candidate_urls(self, base_url: str) -> list[str]:
34+
base = base_url.rstrip('/')
35+
return [
36+
f'{base}/graphql',
37+
f'{base}/api/graphql',
38+
f'{base}/api/v1/graphql',
39+
f'{base}/graphiql',
40+
]
41+
42+
async def fetch_logs_page(
43+
self,
44+
*,
45+
address: Address,
46+
start_block: int | str,
47+
end_block: int | str,
48+
topics: list[str] | None,
49+
cursor: str | None,
50+
page_size: int | None,
51+
gql_headers: Mapping[str, str] | None,
52+
rest_fallback: Callable[[], Awaitable[tuple[list[dict[str, Any]], str | None]]],
53+
) -> tuple[list[dict[str, Any]], str | None]:
54+
if not self._can_use_graphql('logs'):
55+
return await rest_fallback()
56+
57+
endpoint = self._ctx.endpoint_builder.open(
58+
api_key=self._ctx.api_key,
59+
api_kind=self._ctx.api_kind,
60+
network=self._ctx.network,
61+
)
62+
gql_client = self._ctx.gql
63+
gql_builder = self._ctx.gql_builder
64+
if gql_client is None or gql_builder is None:
65+
return await rest_fallback()
66+
67+
query, variables = gql_builder.build_logs_query(
68+
address=str(address),
69+
start_block=start_block,
70+
end_block=end_block,
71+
topics=topics,
72+
after_cursor=cursor,
73+
first=page_size,
74+
)
75+
_, headers = endpoint.filter_and_sign(params=None, headers=None)
76+
if gql_headers:
77+
merged_headers = dict(headers)
78+
merged_headers.update(gql_headers)
79+
headers = merged_headers
80+
81+
async def _do_gql(gql_url: str) -> Any:
82+
if self._ctx.rate_limiter is not None:
83+
await self._ctx.rate_limiter.acquire(
84+
key=f'{self._ctx.api_kind}:{self._ctx.network}:logs:gql'
85+
)
86+
start = monotonic()
87+
try:
88+
return await gql_client.execute(gql_url, query, variables, headers=headers)
89+
finally:
90+
if self._ctx.telemetry is not None:
91+
duration_ms = int((monotonic() - start) * 1000)
92+
await self._ctx.telemetry.record_event(
93+
'logs.get_logs.duration',
94+
{
95+
'api_kind': self._ctx.api_kind,
96+
'network': self._ctx.network,
97+
'duration_ms': duration_ms,
98+
'provider_type': 'graphql',
99+
},
100+
)
101+
102+
last_exc: Exception | None = None
103+
for gql_url in self._candidate_urls(endpoint.base_url):
104+
try:
105+
data: Any
106+
if self._ctx.retry is not None:
107+
108+
async def _runner(url: str = gql_url) -> Any:
109+
return await _do_gql(url)
110+
111+
data = await self._ctx.retry.run(_runner)
112+
else:
113+
data = await _do_gql(gql_url)
114+
115+
items, next_cursor = gql_builder.map_logs_response(data)
116+
if self._ctx.telemetry is not None:
117+
await self._ctx.telemetry.record_event(
118+
'logs.get_logs.ok',
119+
{
120+
'api_kind': self._ctx.api_kind,
121+
'network': self._ctx.network,
122+
'items': len(items),
123+
'provider_type': 'graphql',
124+
},
125+
)
126+
return items, next_cursor
127+
except Exception as exc: # noqa: BLE001
128+
last_exc = exc
129+
continue
130+
131+
if self._ctx.telemetry is not None and last_exc is not None:
132+
await self._ctx.telemetry.record_error(
133+
'logs.get_logs.error',
134+
last_exc,
135+
{
136+
'api_kind': self._ctx.api_kind,
137+
'network': self._ctx.network,
138+
'provider_type': 'graphql',
139+
},
140+
)
141+
142+
return await rest_fallback()
143+
144+
async def fetch_transaction_by_hash(
145+
self,
146+
*,
147+
txhash: TxHash,
148+
cache_key: str,
149+
cache_ttl_seconds: int,
150+
rest_fallback: Callable[[], Awaitable[dict[str, Any]]],
151+
) -> dict[str, Any]:
152+
if not self._can_use_graphql('transaction_by_hash'):
153+
return await rest_fallback()
154+
155+
endpoint = self._ctx.endpoint_builder.open(
156+
api_key=self._ctx.api_key,
157+
api_kind=self._ctx.api_kind,
158+
network=self._ctx.network,
159+
)
160+
gql_client = self._ctx.gql
161+
gql_builder = self._ctx.gql_builder
162+
if gql_client is None or gql_builder is None:
163+
return await rest_fallback()
164+
165+
query, variables = gql_builder.build_transaction_by_hash_query(txhash=str(txhash))
166+
_, headers = endpoint.filter_and_sign(params=None, headers=None)
167+
168+
async def _do_gql(gql_url: str) -> Any:
169+
if self._ctx.rate_limiter is not None:
170+
await self._ctx.rate_limiter.acquire(
171+
key=f'{self._ctx.api_kind}:{self._ctx.network}:tx:gql'
172+
)
173+
start = monotonic()
174+
try:
175+
return await gql_client.execute(gql_url, query, variables, headers=headers)
176+
finally:
177+
if self._ctx.telemetry is not None:
178+
duration_ms = int((monotonic() - start) * 1000)
179+
await self._ctx.telemetry.record_event(
180+
'transaction.get_transaction_by_hash.duration',
181+
{
182+
'api_kind': self._ctx.api_kind,
183+
'network': self._ctx.network,
184+
'duration_ms': duration_ms,
185+
'provider_type': 'graphql',
186+
},
187+
)
188+
189+
last_exc: Exception | None = None
190+
for gql_url in self._candidate_urls(endpoint.base_url):
191+
try:
192+
data: Any
193+
if self._ctx.retry is not None:
194+
195+
async def _runner(url: str = gql_url) -> Any:
196+
return await _do_gql(url)
197+
198+
data = await self._ctx.retry.run(_runner)
199+
else:
200+
data = await _do_gql(gql_url)
201+
202+
mapped = gql_builder.map_transaction_response(data)
203+
if isinstance(mapped, dict) and mapped:
204+
if self._ctx.telemetry is not None:
205+
await self._ctx.telemetry.record_event(
206+
'transaction.get_transaction_by_hash.ok',
207+
{
208+
'api_kind': self._ctx.api_kind,
209+
'network': self._ctx.network,
210+
'provider_type': 'graphql',
211+
},
212+
)
213+
if self._ctx.cache is not None:
214+
await self._ctx.cache.set(cache_key, mapped, ttl_seconds=cache_ttl_seconds)
215+
return mapped
216+
except Exception as exc: # noqa: BLE001
217+
last_exc = exc
218+
continue
219+
220+
if self._ctx.telemetry is not None and last_exc is not None:
221+
await self._ctx.telemetry.record_error(
222+
'transaction.get_transaction_by_hash.error',
223+
last_exc,
224+
{
225+
'api_kind': self._ctx.api_kind,
226+
'network': self._ctx.network,
227+
'provider_type': 'graphql',
228+
},
229+
)
230+
231+
return await rest_fallback()

0 commit comments

Comments
 (0)