kiba-core 0.5.3.dev71__tar.gz → 0.5.3.dev72__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/CHANGELOG.md +4 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/web3/eth_client.py +67 -33
- kiba_core-0.5.3.dev72/core/web3/eth_client_registry.py +46 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/kiba_core.egg-info/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/kiba_core.egg-info/SOURCES.txt +2 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/pyproject.toml +1 -1
- kiba_core-0.5.3.dev72/tests/web3/test_eth_client_registry.py +63 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/web3/test_rest_eth_client.py +80 -2
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/.gitignore +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/README.md +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/database_connection_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/store/database.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/kiba_core.egg-info/requires.txt +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/makefile +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_json_route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/test_database.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/test_logging.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/uv.lock +0 -0
|
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- [MAJOR] Updated `RestEthClient` to raise `RpcServerException` (a `ServerException` keeping the original status code and message, with the original exception as its cause) when the node responds with a 5xx, so callers can tell node failures apart from other server errors; `RequesterTimeoutException` is still raised unchanged. Code catching `InternalServerErrorException` (or `BadGatewayException`, `ServiceUnavailableException`, …) around `RestEthClient` calls must catch `RpcServerException` instead
|
|
12
|
+
- [MINOR] Added `RestEthClient.get_code`
|
|
13
|
+
- [MINOR] Added optional `maxConcurrentRequestCount` and `semaphore` to `RestEthClient` to limit concurrent requests; pass the same `semaphore` to several clients to limit them together (it takes precedence over `maxConcurrentRequestCount`)
|
|
14
|
+
- [MINOR] Added `EthClientRegistry` to register regular, archive and paymaster `RestEthClient`s per chain and pick the archive client for dates older than `archiveThresholdDays` (default 15)
|
|
11
15
|
- [PATCH] Fixed log records from `logging.info`, `warning`, `error`, `critical`, `debug`, `exception`, `stat` and `api` recording their location as `core/logging.py` instead of the calling code; this was only visible in `init_logging`'s text format, since the JSON and basic formats don't output the location
|
|
12
16
|
- [MINOR] Updated `init_logging`, `init_json_logging` and `init_basic_logging` to set the loggers of the third-party libraries core uses (`DEFAULT_EXTERNAL_LOGGER_NAMES`: httpx2, httpcore2, sqlalchemy, aiosqlite, asyncpg, botocore, aiobotocore, azure, apscheduler, web3) to `WARNING`; pass `extraLoggerNames` to quieten more loggers on top of these, or `loggerNames` to replace them (`loggerNames=[]` keeps the previous behaviour). `init_external_loggers` takes the same arguments and now defaults to the same list
|
|
13
17
|
- [MINOR] Added `core.cron` (install with the `cron` extra): `CronJob` schedules (interval with jitter, or crontab) that declare a `jobType`, `CronJobProcessor`s that declare the `jobTypes` they process, and `CronExecutor`, which takes a list of processors, routes each job to the processor for its type (failing at startup if a job type has no processor or more than one), runs jobs on an APScheduler scheduler, logs each run as a `CRON` api log with its status and duration, and keeps running when a job fails; also added `ApiRequestCronJob` with `ApiRequestCronJobProcessor` (needs the `requester` extra) for jobs that call a URL
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import typing
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -24,8 +25,11 @@ from core.exceptions import BadRequestException
|
|
|
24
25
|
from core.exceptions import ClientException
|
|
25
26
|
from core.exceptions import KibaException
|
|
26
27
|
from core.exceptions import NotFoundException
|
|
28
|
+
from core.exceptions import ServerException
|
|
27
29
|
from core.exceptions import TooManyRequestsException
|
|
28
30
|
from core.requester import Requester
|
|
31
|
+
from core.requester.requester import RequesterTimeoutException
|
|
32
|
+
from core.requester.requester import ResponseException
|
|
29
33
|
from core.util import chain_util
|
|
30
34
|
from core.util import json_util
|
|
31
35
|
from core.util.typing_util import JsonObject
|
|
@@ -61,6 +65,11 @@ class TransactionFailedException(KibaException):
|
|
|
61
65
|
return output
|
|
62
66
|
|
|
63
67
|
|
|
68
|
+
class RpcServerException(ServerException):
|
|
69
|
+
def __init__(self, message: str | None = None, statusCode: int = 500) -> None:
|
|
70
|
+
super().__init__(message=message or 'RPC Server Error', statusCode=statusCode)
|
|
71
|
+
|
|
72
|
+
|
|
64
73
|
class EthClientInterface:
|
|
65
74
|
def __init__(self, web3Connection: Web3, chainId: int, isTestnet: bool = False) -> None:
|
|
66
75
|
self.w3 = web3Connection
|
|
@@ -297,12 +306,26 @@ class Web3EthClient(EthClientInterface):
|
|
|
297
306
|
|
|
298
307
|
class RestEthClient(EthClientInterface):
|
|
299
308
|
# NOTE(krishan711): find docs at https://eth.wiki/json-rpc/API
|
|
300
|
-
def __init__(
|
|
309
|
+
def __init__(
|
|
310
|
+
self,
|
|
311
|
+
url: str,
|
|
312
|
+
requester: Requester,
|
|
313
|
+
chainId: int,
|
|
314
|
+
isTestnet: bool = False,
|
|
315
|
+
shouldBackoffRetryOnRateLimit: bool = True,
|
|
316
|
+
retryLimit: int = 10,
|
|
317
|
+
maxConcurrentRequestCount: int | None = None,
|
|
318
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
319
|
+
) -> None:
|
|
301
320
|
super().__init__(web3Connection=Web3(), chainId=chainId, isTestnet=isTestnet)
|
|
302
321
|
self.url = url
|
|
303
322
|
self.requester = requester
|
|
304
323
|
self.shouldBackoffRetryOnRateLimit = shouldBackoffRetryOnRateLimit
|
|
305
324
|
self.retryLimit = retryLimit
|
|
325
|
+
# NOTE(krishan711): pass the same semaphore to several clients to cap their combined concurrent requests, or set maxConcurrentRequestCount to cap just this client
|
|
326
|
+
if semaphore is None and maxConcurrentRequestCount is not None:
|
|
327
|
+
semaphore = asyncio.Semaphore(maxConcurrentRequestCount)
|
|
328
|
+
self.semaphore = semaphore
|
|
306
329
|
self.w3 = Web3()
|
|
307
330
|
|
|
308
331
|
@staticmethod
|
|
@@ -310,39 +333,45 @@ class RestEthClient(EthClientInterface):
|
|
|
310
333
|
return int(value, 16)
|
|
311
334
|
|
|
312
335
|
async def _make_request(self, method: str, params: ListAny | None = None) -> DictStrAny:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
async with self.semaphore or contextlib.nullcontext():
|
|
337
|
+
retryCount = 0
|
|
338
|
+
initialBackoffSeconds = 1.0
|
|
339
|
+
responseDict: DictStrAny = {}
|
|
340
|
+
while True:
|
|
341
|
+
try:
|
|
342
|
+
response = await self.requester.post_json(
|
|
343
|
+
url=self.url,
|
|
344
|
+
dataDict={'jsonrpc': '2.0', 'method': method, 'params': params or [], 'id': None},
|
|
345
|
+
timeout=10,
|
|
346
|
+
)
|
|
347
|
+
responseDict = response.json()
|
|
348
|
+
if responseDict.get('error'):
|
|
349
|
+
errorMessage = responseDict['error'].get('message') or responseDict['error'].get('details') or json_util.dumps(responseDict['error'])
|
|
350
|
+
raise BadRequestException(message=errorMessage)
|
|
351
|
+
except ClientException as exception:
|
|
352
|
+
# NOTE(krishan711): this is just here to debug 429s from coinbase, remove when done
|
|
353
|
+
logging.info(f'caught exception on _make_request: {exception!s}')
|
|
354
|
+
exceptionMessage = exception.message or ''
|
|
355
|
+
if (
|
|
356
|
+
isinstance(exception, TooManyRequestsException)
|
|
357
|
+
# NOTE(krishan711): sometimes coinbase returns an error message with 200 status
|
|
358
|
+
or 'over rate limit' in exceptionMessage
|
|
359
|
+
or '429 Too Many Requests' in exceptionMessage
|
|
360
|
+
):
|
|
361
|
+
if not self.shouldBackoffRetryOnRateLimit or retryCount >= self.retryLimit:
|
|
362
|
+
raise
|
|
363
|
+
retryCount += 1
|
|
364
|
+
exponentialBackoffSeconds = initialBackoffSeconds * (2 ** (retryCount - 1))
|
|
365
|
+
logging.info(f'Retrying {method} after {exponentialBackoffSeconds} seconds due to rate limit exceeded: {exception!s}')
|
|
366
|
+
await asyncio.sleep(exponentialBackoffSeconds)
|
|
367
|
+
continue
|
|
368
|
+
raise
|
|
369
|
+
except (ServerException, ResponseException) as exception:
|
|
370
|
+
# NOTE(krishan711): timeouts are raised from the requester rather than the node so they keep their own type
|
|
371
|
+
if isinstance(exception, RequesterTimeoutException) or exception.statusCode < 500: # noqa: PLR2004
|
|
338
372
|
raise
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
logging.info(f'Retrying {method} after {exponentialBackoffSeconds} seconds due to rate limit exceeded: {exception!s}')
|
|
342
|
-
await asyncio.sleep(exponentialBackoffSeconds)
|
|
343
|
-
continue
|
|
344
|
-
raise
|
|
345
|
-
return responseDict
|
|
373
|
+
raise RpcServerException(message=exception.message, statusCode=exception.statusCode) from exception
|
|
374
|
+
return responseDict
|
|
346
375
|
|
|
347
376
|
async def get_latest_block_number(self) -> int:
|
|
348
377
|
response = await self._make_request(method='eth_blockNumber')
|
|
@@ -394,6 +423,10 @@ class RestEthClient(EthClientInterface):
|
|
|
394
423
|
raise NotFoundException
|
|
395
424
|
return typing.cast(TxReceipt, method_formatters.PYTHONIC_RESULT_FORMATTERS[RPC.eth_getTransactionReceipt](response['result']))
|
|
396
425
|
|
|
426
|
+
async def get_code(self, address: str, blockNumber: int | None = None) -> str:
|
|
427
|
+
response = await self._make_request(method='eth_getCode', params=[address, hex(blockNumber) if blockNumber is not None else 'latest'])
|
|
428
|
+
return str(response.get('result', '0x'))
|
|
429
|
+
|
|
397
430
|
async def get_log_entries(
|
|
398
431
|
self,
|
|
399
432
|
topics: list[str] | None = None,
|
|
@@ -481,3 +514,4 @@ class RestEthClient(EthClientInterface):
|
|
|
481
514
|
transactionData = '0x' + transactionData
|
|
482
515
|
response = await self._make_request(method='eth_sendRawTransaction', params=[transactionData])
|
|
483
516
|
return typing.cast(str, response['result'])
|
|
517
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from core.exceptions import InternalServerErrorException
|
|
4
|
+
from core.util import date_util
|
|
5
|
+
from core.web3.eth_client import RestEthClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EthClientRegistry:
|
|
9
|
+
def __init__(self, archiveThresholdDays: int = 15) -> None:
|
|
10
|
+
self.archiveThresholdDays = archiveThresholdDays
|
|
11
|
+
self.ethClientsByChainId: dict[int, RestEthClient] = {}
|
|
12
|
+
self.archiveEthClientsByChainId: dict[int, RestEthClient] = {}
|
|
13
|
+
self.paymasterEthClientsByChainId: dict[int, RestEthClient] = {}
|
|
14
|
+
|
|
15
|
+
def register_client(self, client: RestEthClient) -> None:
|
|
16
|
+
self.ethClientsByChainId[client.chainId] = client
|
|
17
|
+
|
|
18
|
+
def register_archive_client(self, client: RestEthClient) -> None:
|
|
19
|
+
self.archiveEthClientsByChainId[client.chainId] = client
|
|
20
|
+
|
|
21
|
+
def register_paymaster_client(self, client: RestEthClient) -> None:
|
|
22
|
+
self.paymasterEthClientsByChainId[client.chainId] = client
|
|
23
|
+
|
|
24
|
+
def get_client(self, chainId: int, date: datetime.date | None = None) -> RestEthClient:
|
|
25
|
+
# NOTE(krishan711): regular nodes may have pruned state older than the threshold so those reads go to the archive node
|
|
26
|
+
dateDiff = date_util.calculate_diff_days(startDate=date_util.datetime_from_date(date=date), endDate=date_util.datetime_from_now()) if date is not None else 0
|
|
27
|
+
if dateDiff > self.archiveThresholdDays:
|
|
28
|
+
return self.get_archive_client(chainId=chainId)
|
|
29
|
+
return self.get_regular_client(chainId=chainId)
|
|
30
|
+
|
|
31
|
+
def get_paymaster_client(self, chainId: int) -> RestEthClient:
|
|
32
|
+
if chainId not in self.paymasterEthClientsByChainId:
|
|
33
|
+
raise InternalServerErrorException(f'Chain {chainId} does not have a paymaster client')
|
|
34
|
+
return self.paymasterEthClientsByChainId[chainId]
|
|
35
|
+
|
|
36
|
+
def get_archive_client(self, chainId: int) -> RestEthClient:
|
|
37
|
+
if chainId not in self.archiveEthClientsByChainId:
|
|
38
|
+
raise InternalServerErrorException(f'Chain {chainId} does not have an archive client')
|
|
39
|
+
return self.archiveEthClientsByChainId[chainId]
|
|
40
|
+
|
|
41
|
+
def get_regular_client(self, chainId: int) -> RestEthClient:
|
|
42
|
+
if chainId in self.ethClientsByChainId:
|
|
43
|
+
return self.ethClientsByChainId[chainId]
|
|
44
|
+
if chainId in self.archiveEthClientsByChainId:
|
|
45
|
+
return self.archiveEthClientsByChainId[chainId]
|
|
46
|
+
raise InternalServerErrorException(f'Chain {chainId} does not have a regular client or an archive client')
|
|
@@ -88,6 +88,7 @@ core/util/url_util.py
|
|
|
88
88
|
core/util/value_holder.py
|
|
89
89
|
core/web3/__init__.py
|
|
90
90
|
core/web3/eth_client.py
|
|
91
|
+
core/web3/eth_client_registry.py
|
|
91
92
|
core/web3/multicall3.py
|
|
92
93
|
kiba_core.egg-info/PKG-INFO
|
|
93
94
|
kiba_core.egg-info/SOURCES.txt
|
|
@@ -132,4 +133,5 @@ tests/util/test_process_util.py
|
|
|
132
133
|
tests/util/test_string_util.py
|
|
133
134
|
tests/util/test_url_util.py
|
|
134
135
|
tests/web3/__init__.py
|
|
136
|
+
tests/web3/test_eth_client_registry.py
|
|
135
137
|
tests/web3/test_rest_eth_client.py
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from core.exceptions import InternalServerErrorException
|
|
6
|
+
from core.requester import Requester
|
|
7
|
+
from core.util import date_util
|
|
8
|
+
from core.web3.eth_client import RestEthClient
|
|
9
|
+
from core.web3.eth_client_registry import EthClientRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _client(chainId: int) -> RestEthClient:
|
|
13
|
+
return RestEthClient(url=f'https://rpc-{chainId}.test', requester=Requester(), chainId=chainId)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _days_ago(days: int) -> datetime.date:
|
|
17
|
+
return date_util.datetime_from_now(days=-days).date()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_client_uses_regular_client_for_recent_dates():
|
|
21
|
+
registry = EthClientRegistry()
|
|
22
|
+
regularClient = _client(chainId=8453)
|
|
23
|
+
archiveClient = _client(chainId=8453)
|
|
24
|
+
registry.register_client(client=regularClient)
|
|
25
|
+
registry.register_archive_client(client=archiveClient)
|
|
26
|
+
assert registry.get_client(chainId=8453) is regularClient
|
|
27
|
+
assert registry.get_client(chainId=8453, date=_days_ago(days=15)) is regularClient
|
|
28
|
+
assert registry.get_client(chainId=8453, date=_days_ago(days=16)) is archiveClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_client_respects_archive_threshold():
|
|
32
|
+
registry = EthClientRegistry(archiveThresholdDays=2)
|
|
33
|
+
regularClient = _client(chainId=8453)
|
|
34
|
+
archiveClient = _client(chainId=8453)
|
|
35
|
+
registry.register_client(client=regularClient)
|
|
36
|
+
registry.register_archive_client(client=archiveClient)
|
|
37
|
+
assert registry.get_client(chainId=8453, date=_days_ago(days=2)) is regularClient
|
|
38
|
+
assert registry.get_client(chainId=8453, date=_days_ago(days=3)) is archiveClient
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_get_regular_client_falls_back_to_archive_client():
|
|
42
|
+
registry = EthClientRegistry()
|
|
43
|
+
archiveClient = _client(chainId=1)
|
|
44
|
+
registry.register_archive_client(client=archiveClient)
|
|
45
|
+
assert registry.get_regular_client(chainId=1) is archiveClient
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_missing_clients_raise():
|
|
49
|
+
registry = EthClientRegistry()
|
|
50
|
+
registry.register_client(client=_client(chainId=1))
|
|
51
|
+
with pytest.raises(InternalServerErrorException):
|
|
52
|
+
registry.get_regular_client(chainId=8453)
|
|
53
|
+
with pytest.raises(InternalServerErrorException):
|
|
54
|
+
registry.get_archive_client(chainId=1)
|
|
55
|
+
with pytest.raises(InternalServerErrorException):
|
|
56
|
+
registry.get_paymaster_client(chainId=1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_get_paymaster_client():
|
|
60
|
+
registry = EthClientRegistry()
|
|
61
|
+
paymasterClient = _client(chainId=8453)
|
|
62
|
+
registry.register_paymaster_client(client=paymasterClient)
|
|
63
|
+
assert registry.get_paymaster_client(chainId=8453) is paymasterClient
|
|
@@ -3,9 +3,10 @@ import pytest
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
from collections.abc import MutableMapping
|
|
5
5
|
|
|
6
|
-
from core.exceptions import BadRequestException, NotFoundException, TooManyRequestsException
|
|
6
|
+
from core.exceptions import BadRequestException, InternalServerErrorException, NotFoundException, ServerException, ServiceUnavailableException, TooManyRequestsException
|
|
7
7
|
from core.requester import Requester, KibaResponse
|
|
8
|
-
from core.
|
|
8
|
+
from core.requester.requester import RequesterTimeoutException, ResponseException
|
|
9
|
+
from core.web3.eth_client import RestEthClient, RpcServerException
|
|
9
10
|
from core.util.typing_util import Json
|
|
10
11
|
|
|
11
12
|
|
|
@@ -493,6 +494,83 @@ class TestRestEthClient:
|
|
|
493
494
|
with pytest.raises(TooManyRequestsException):
|
|
494
495
|
await client.get_latest_block_number()
|
|
495
496
|
|
|
497
|
+
@pytest.mark.asyncio
|
|
498
|
+
@pytest.mark.parametrize('exception', [
|
|
499
|
+
InternalServerErrorException(message='{"jsonrpc":"2.0","error":{"code":-32000,"message":"internal error"}}'),
|
|
500
|
+
ServiceUnavailableException(message='upstream connect error or disconnect/reset before headers'),
|
|
501
|
+
ResponseException(message='unknown', statusCode=599),
|
|
502
|
+
])
|
|
503
|
+
async def test_server_error_raises_rpc_server_exception(self, client, mock_requester, exception):
|
|
504
|
+
call_count = 0
|
|
505
|
+
async def mock_post_json_server_error(*args, **kwargs):
|
|
506
|
+
nonlocal call_count
|
|
507
|
+
call_count += 1
|
|
508
|
+
raise exception
|
|
509
|
+
mock_requester.post_json = mock_post_json_server_error
|
|
510
|
+
with pytest.raises(RpcServerException) as exceptionInfo:
|
|
511
|
+
await client.get_latest_block_number()
|
|
512
|
+
assert exceptionInfo.value.statusCode == exception.statusCode
|
|
513
|
+
assert exceptionInfo.value.message == exception.message
|
|
514
|
+
assert exceptionInfo.value.__cause__ is exception
|
|
515
|
+
assert isinstance(exceptionInfo.value, ServerException)
|
|
516
|
+
assert call_count == 1
|
|
517
|
+
|
|
518
|
+
@pytest.mark.asyncio
|
|
519
|
+
async def test_timeout_keeps_requester_timeout_exception(self, client, mock_requester):
|
|
520
|
+
async def mock_post_json_timeout(*args, **kwargs):
|
|
521
|
+
raise RequesterTimeoutException(method='POST', url='https://test-rpc-url.com', timeoutSeconds=10, durationSeconds=10, timeoutType='ReadTimeout')
|
|
522
|
+
mock_requester.post_json = mock_post_json_timeout
|
|
523
|
+
with pytest.raises(RequesterTimeoutException):
|
|
524
|
+
await client.get_latest_block_number()
|
|
525
|
+
|
|
526
|
+
@pytest.mark.asyncio
|
|
527
|
+
async def test_get_code_success(self, client, mock_requester):
|
|
528
|
+
mock_requester.responses['eth_getCode'] = {'jsonrpc': '2.0', 'result': '0x6080', 'id': None}
|
|
529
|
+
result = await client.get_code(address='0x1234567890123456789012345678901234567890', blockNumber=436)
|
|
530
|
+
assert result == '0x6080'
|
|
531
|
+
assert mock_requester.requests_made[0]['dataDict']['params'] == ['0x1234567890123456789012345678901234567890', hex(436)]
|
|
532
|
+
|
|
533
|
+
@pytest.mark.asyncio
|
|
534
|
+
async def test_get_code_defaults_to_latest(self, client, mock_requester):
|
|
535
|
+
mock_requester.responses['eth_getCode'] = {'jsonrpc': '2.0', 'result': '0x', 'id': None}
|
|
536
|
+
result = await client.get_code(address='0x1234567890123456789012345678901234567890')
|
|
537
|
+
assert result == '0x'
|
|
538
|
+
assert mock_requester.requests_made[0]['dataDict']['params'] == ['0x1234567890123456789012345678901234567890', 'latest']
|
|
539
|
+
|
|
540
|
+
@staticmethod
|
|
541
|
+
async def _max_concurrent_request_count(mock_requester, clients) -> int:
|
|
542
|
+
activeCount = 0
|
|
543
|
+
maxActiveCount = 0
|
|
544
|
+
async def mock_post_json_slow(*args, **kwargs):
|
|
545
|
+
nonlocal activeCount, maxActiveCount
|
|
546
|
+
activeCount += 1
|
|
547
|
+
maxActiveCount = max(maxActiveCount, activeCount)
|
|
548
|
+
await asyncio.sleep(0.01)
|
|
549
|
+
activeCount -= 1
|
|
550
|
+
return MockResponse({'jsonrpc': '2.0', 'result': hex(436), 'id': None})
|
|
551
|
+
mock_requester.post_json = mock_post_json_slow
|
|
552
|
+
results = await asyncio.gather(*[client.get_latest_block_number() for client in clients for _ in range(3)])
|
|
553
|
+
assert results == [436] * 3 * len(clients)
|
|
554
|
+
return maxActiveCount
|
|
555
|
+
|
|
556
|
+
@pytest.mark.asyncio
|
|
557
|
+
async def test_requests_are_not_limited_by_default(self, client, mock_requester):
|
|
558
|
+
assert client.semaphore is None
|
|
559
|
+
assert await self._max_concurrent_request_count(mock_requester=mock_requester, clients=[client]) == 3
|
|
560
|
+
|
|
561
|
+
@pytest.mark.asyncio
|
|
562
|
+
async def test_max_concurrent_request_count_limits_each_client(self, mock_requester):
|
|
563
|
+
clients = [RestEthClient(url='https://test-rpc-url.com', requester=mock_requester, chainId=chainId, maxConcurrentRequestCount=2) for chainId in (1, 8453)]
|
|
564
|
+
assert clients[0].semaphore is not clients[1].semaphore
|
|
565
|
+
assert await self._max_concurrent_request_count(mock_requester=mock_requester, clients=clients) == 4
|
|
566
|
+
|
|
567
|
+
@pytest.mark.asyncio
|
|
568
|
+
async def test_shared_semaphore_limits_clients_together(self, mock_requester):
|
|
569
|
+
semaphore = asyncio.Semaphore(2)
|
|
570
|
+
clients = [RestEthClient(url='https://test-rpc-url.com', requester=mock_requester, chainId=chainId, semaphore=semaphore, maxConcurrentRequestCount=5) for chainId in (1, 8453)]
|
|
571
|
+
assert all(client.semaphore is semaphore for client in clients)
|
|
572
|
+
assert await self._max_concurrent_request_count(mock_requester=mock_requester, clients=clients) == 2
|
|
573
|
+
|
|
496
574
|
@pytest.mark.asyncio
|
|
497
575
|
async def test_fill_transaction_params_all_provided(self, client, mock_requester):
|
|
498
576
|
params = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/core/api/middleware/server_headers_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev71 → kiba_core-0.5.3.dev72}/tests/api/test_exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|