From ba41ecdc276fb4696205aa6525c743b7ee3ace7f Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 13 Feb 2025 14:09:25 -0300
Subject: [PATCH 01/30] Universal config generator
---
src/dipdup/_survey.py | 503 ------------------
src/dipdup/config/__init__.py | 9 +-
src/dipdup/config/coinbase.py | 2 +-
src/dipdup/config/evm.py | 2 +-
src/dipdup/config/evm_etherscan.py | 2 +-
src/dipdup/config/evm_events.py | 2 +-
src/dipdup/config/evm_node.py | 2 +-
src/dipdup/config/evm_subsquid.py | 2 +-
src/dipdup/config/evm_transactions.py | 2 +-
src/dipdup/config/http.py | 2 +-
src/dipdup/config/ipfs.py | 2 +-
src/dipdup/config/starknet.py | 2 +-
src/dipdup/config/starknet_events.py | 2 +-
src/dipdup/config/starknet_node.py | 2 +-
src/dipdup/config/starknet_subsquid.py | 2 +-
src/dipdup/config/substrate_events.py | 2 +-
src/dipdup/config/substrate_node.py | 2 +-
src/dipdup/config/substrate_subscan.py | 2 +-
src/dipdup/config/substrate_subsquid.py | 2 +-
src/dipdup/config/tezos.py | 2 +-
src/dipdup/config/tezos_big_maps.py | 2 +-
src/dipdup/config/tezos_events.py | 2 +-
src/dipdup/config/tezos_head.py | 2 +-
src/dipdup/config/tezos_operations.py | 4 +-
src/dipdup/config/tezos_token_balances.py | 2 +-
src/dipdup/config/tezos_token_transfers.py | 2 +-
src/dipdup/config/tezos_tzkt.py | 2 +-
src/dipdup/config/tzip_metadata.py | 2 +-
src/dipdup/datasources/starknet_node.py | 8 +-
src/dipdup/indexes/starknet_events/fetcher.py | 10 +-
src/dipdup/project.py | 154 +++++-
31 files changed, 173 insertions(+), 565 deletions(-)
delete mode 100644 src/dipdup/_survey.py
diff --git a/src/dipdup/_survey.py b/src/dipdup/_survey.py
deleted file mode 100644
index 5d88c16d6..000000000
--- a/src/dipdup/_survey.py
+++ /dev/null
@@ -1,503 +0,0 @@
-from typing import Any
-from typing import TypedDict
-
-import survey # type: ignore[import-untyped]
-
-from dipdup.cli import echo
-from dipdup.install import ask
-
-
-class Pattern(TypedDict):
- destination: str
- entrypoint: str
-
-
-class Handler(TypedDict):
- name: str | None
- callback: str | None
- contract: str | None
- path: str | None
- tag: str | None
- pattern: Pattern | None
- to: str | None
- method: str | None
-
-
-class Datasource(TypedDict):
- name: str
- kind: str
- url: str
- ws_url: str | None
- api_key: str | None
-
-
-class Contract(TypedDict):
- name: str
- kind: str
- address: str
- typename: str
-
-
-class Index(TypedDict):
- name: str
- kind: str
- datasources: list[str]
- handlers: list[Handler] | None
- skip_history: str | None
- first_level: int | None
- last_level: int | None
- callback: str | None
- types: list[str] | None
- contracts: list[str] | None
-
-
-class DipDupSurveyConfig(TypedDict):
- datasources: list[Datasource]
- contracts: list[Contract] | None
- indexes: list[Index]
-
-
-class DatasourceConfig(TypedDict):
- kind: str
- requires_api_key: bool
- default_url: str
- name: str
-
-
-class IndexerConfig(TypedDict):
- handler_fields: list[str]
- optional_fields: dict[str, str | int]
- kind: str
-
-
-class BlockchainConfig(TypedDict):
- datasources: list[DatasourceConfig]
- contract_kind: str
- indexes: dict[str, IndexerConfig]
-
-
-CONFIG_STRUCTURE: dict[str, BlockchainConfig] = {
- 'evm': {
- 'datasources': [
- {
- 'kind': 'evm.subsquid',
- 'requires_api_key': False,
- 'default_url': 'https://v2.archive.subsquid.io/network/ethereum-mainnet',
- 'name': 'subsquid',
- },
- {
- 'kind': 'evm.node',
- 'requires_api_key': True,
- 'default_url': 'https://eth-mainnet.g.alchemy.com/v2',
- 'name': 'node',
- },
- {
- 'kind': 'evm.etherscan',
- 'requires_api_key': True,
- 'default_url': 'https://api.etherscan.io/api',
- 'name': 'etherscan',
- },
- ],
- 'contract_kind': 'evm',
- 'indexes': {
- 'events': {
- 'handler_fields': ['callback', 'contract', 'name'],
- 'optional_fields': {},
- 'kind': 'evm.events',
- },
- 'transactions': {
- 'handler_fields': ['callback', 'to', 'method'],
- 'optional_fields': {
- 'first_level': 'integer',
- },
- 'kind': 'evm.transactions',
- },
- },
- },
- 'tezos': {
- 'datasources': [
- {
- 'kind': 'tezos.tzkt',
- 'requires_api_key': False,
- 'default_url': 'https://api.ghostnet.tzkt.io',
- 'name': 'tzkt',
- }
- ],
- 'contract_kind': 'tezos',
- 'indexes': {
- 'big_maps': {
- 'handler_fields': ['callback', 'contract', 'path'],
- 'optional_fields': {
- 'skip_history': 'select',
- },
- 'kind': 'tezos.big_maps',
- },
- 'events': {
- 'handler_fields': ['callback', 'contract', 'tag'],
- 'optional_fields': {},
- 'kind': 'tezos.events',
- },
- 'head': {
- 'handler_fields': [],
- 'optional_fields': {
- 'callback': 'string',
- },
- 'kind': 'tezos.head',
- },
- 'operations': {
- 'handler_fields': ['callback', 'pattern'],
- 'optional_fields': {},
- 'kind': 'tezos.operations',
- },
- 'operations_unfiltered': {
- 'handler_fields': [],
- 'optional_fields': {
- 'types': 'select',
- 'callback': 'string',
- 'first_level': 'integer',
- 'last_level': 'integer',
- },
- 'kind': 'tezos.operations_unfiltered',
- },
- 'token_balances': {
- 'handler_fields': ['callback', 'contract'],
- 'optional_fields': {},
- 'kind': 'tezos.token_balances',
- },
- 'token_transfers': {
- 'handler_fields': ['callback', 'contract'],
- 'optional_fields': {},
- 'kind': 'tezos.token_transfers',
- },
- },
- },
- 'starknet': {
- 'datasources': [
- {
- 'kind': 'starknet.subsquid',
- 'requires_api_key': False,
- 'default_url': 'https://v2.archive.subsquid.io/network/starknet-mainnet',
- 'name': 'subsquid',
- },
- {
- 'kind': 'starknet.node',
- 'requires_api_key': True,
- 'default_url': 'https://starknet-mainnet.g.alchemy.com/v2',
- 'name': 'node',
- },
- ],
- 'contract_kind': 'starknet',
- 'indexes': {
- 'events': {
- 'handler_fields': ['callback', 'contract', 'name'],
- 'optional_fields': {},
- 'kind': 'starknet.events',
- }
- },
- },
-}
-
-
-def prompt_anyof(
- question: str,
- options: tuple[str, ...],
- comments: tuple[str, ...],
- default: int,
-) -> tuple[int, str]:
- """Ask user to choose one of the options; returns index and value"""
- from tabulate import tabulate
-
- table = tabulate(
- zip(options, comments, strict=False),
- tablefmt='plain',
- )
- index = survey.routines.select(
- question + '\n',
- options=table.split('\n'),
- index=default,
- )
- return index, options[index]
-
-
-# Helper function to generate comments for datasource options
-def get_datasource_comments(datasources: list[str]) -> tuple[str, ...]:
- default_comments = {
- 'evm.subsquid': 'Use Subsquid as your datasource for EVM.',
- 'evm.node': 'Connect to an EVM node.',
- 'evm.etherscan': 'Fetch ABI from Etherscan.',
- 'tezos.tzkt': 'Use TzKT API for Tezos.',
- 'starknet.subsquid': 'Use Subsquid for Starknet.',
- 'starknet.node': 'Connect to a Starknet node.',
- }
- return tuple(default_comments.get(option, 'No description available') for option in datasources)
-
-
-# Helper function to generate comments for indexes options
-def get_index_comments(indexes: dict[str, IndexerConfig]) -> tuple[str, ...]:
- default_comments: dict[str, str] = {
- 'evm.events': 'Listen to EVM blockchain events.',
- 'evm.transactions': 'Track EVM blockchain transactions.',
- 'tezos.big_maps': 'Monitor changes in Tezos big maps.',
- 'tezos.events': 'Track specific events in Tezos.',
- 'tezos.head': 'Monitor Tezos chain head updates.',
- 'tezos.operations': 'Track operations on Tezos blockchain.',
- 'tezos.operations_unfiltered': 'Monitor unfiltered Tezos operations.',
- 'tezos.token_balances': 'Track Tezos token balances.',
- 'tezos.token_transfers': 'Monitor token transfers on Tezos.',
- 'starknet.events': 'Listen to Starknet blockchain events.',
- }
- comments = []
- for _, config in indexes.items():
- kind = config.get('kind')
- comment = default_comments.get(kind, 'No description available') # type: ignore[arg-type]
- comments.append(comment)
- return tuple(comments)
-
-
-def query_handlers(
- contract_names: list[str],
- additional_fields: list[str] | None,
-) -> list[Handler] | None:
-
- handlers: list[Handler] = []
-
- if not additional_fields:
- return None
-
- first = True
- while first or ask('Add another handler?', False):
- first = False
-
- handler: Handler = {
- 'callback': '',
- 'path': None,
- 'tag': None,
- 'pattern': None,
- 'name': None,
- 'contract': None,
- 'to': None,
- 'method': None,
- }
-
- # Prompt for additional fields
- for field in additional_fields:
- if field in ['contract', 'to']:
- handler[field] = prompt_anyof( # type: ignore[literal-required]
- 'Choose contract for the handler',
- tuple(contract_names),
- ('Contract to listen to',) * len(contract_names),
- 0,
- )[1]
- elif field == 'pattern':
- handler['pattern'] = {
- 'destination': prompt_anyof(
- 'Choose contract for the handler',
- tuple(contract_names),
- ('Contract to listen to',) * len(contract_names),
- 0,
- )[1],
- 'entrypoint': validate_non_empty_input(
- survey.routines.input('Enter pattern entrypoint: ', value=''), 'entrypoint'
- ),
- }
- else:
- handler[field] = validate_non_empty_input( # type: ignore[literal-required]
- survey.routines.input(f'Enter handler {field}: ', value=''),
- field,
- )
-
- handlers.append(handler)
-
- return handlers
-
-
-def query_optional_fields(optional_fields: dict[str, str | int]) -> dict[str, Any]:
- field_values: dict[str, Any] = {}
- for field, field_type in optional_fields.items():
- if field_type == 'select':
- if field == 'types':
- types: list[str] = []
- while True:
- _, type = prompt_anyof(
- 'Select operation type',
- ('origination', 'transaction', 'migration'),
- ('origination', 'transaction', 'migration'),
- 0,
- )
- types.append(type)
- if not ask('Add another type?', False):
- break
- field_values[field] = types
- else:
- _, field_values[field] = prompt_anyof(
- f'Select {field}',
- ('never', 'always', 'auto'),
- ('Never', 'Always', 'Auto'),
- 0,
- )
- else:
- field_values[field] = survey.routines.input(f'Enter {field}: ')
-
- return field_values
-
-
-def validate_non_empty_input(input_value: str, field_name: str) -> str:
- while not input_value.strip():
- echo(f'{field_name} cannot be empty. Please enter a valid value.', fg='red')
- input_value = survey.routines.input(f'Enter {field_name}: ')
- return input_value
-
-
-def filter_index_options(
- indexes: tuple[str, ...],
- index_comments: tuple[str, ...],
- contracts: list[Contract],
- blockchain: str,
-) -> tuple[tuple[str, ...], tuple[str, ...]]:
- blockchain_config = CONFIG_STRUCTURE[blockchain]
- index_dict = blockchain_config['indexes']
- required_contract_fields = {'contract', 'to', 'pattern'}
- if not contracts:
- valid_indexes = [
- idx
- for idx, index in enumerate(indexes)
- if not required_contract_fields.intersection(set(index_dict[index].get('handler_fields', []))) or contracts
- ]
-
- indexes = tuple(indexes[idx] for idx in valid_indexes)
- index_comments = tuple(index_comments[idx] for idx in valid_indexes)
-
- return indexes, index_comments
-
-
-def query_survey_config(blockchain: str) -> DipDupSurveyConfig:
- blockchain_config = CONFIG_STRUCTURE[blockchain]
- datasources: list[Datasource] = []
- contracts: list[Contract] = []
- indexes: list[Index] = []
- survey_config = DipDupSurveyConfig(
- datasources=datasources,
- contracts=contracts,
- indexes=indexes,
- )
-
- first = True
- while ask(f'Add {'' if first else 'another '}datasource?', first):
- first = False
-
- datasource_options = blockchain_config['datasources']
- datasource_comments = get_datasource_comments([ds['kind'] for ds in datasource_options])
-
- selected_index, _ = prompt_anyof(
- f'Select the datasource kind for {blockchain}:',
- tuple(ds['name'] for ds in datasource_options),
- datasource_comments,
- 0,
- )
- selected_datasource = datasource_options[selected_index]
- datasource_kind = selected_datasource['kind']
-
- final_url = validate_non_empty_input(
- survey.routines.input('Enter Datasource URL: ', value=selected_datasource.get('default_url', '')),
- 'Datasource URL',
- )
- api_key = None
- ws_url = None
-
- if selected_datasource['requires_api_key']:
- api_key = validate_non_empty_input(
- survey.routines.input('Enter API key: ', value=''),
- 'API key',
- )
- if 'node' in datasource_kind:
- final_url = '${NODE_URL:-' + final_url + '}/${NODE_API_KEY:-' + api_key + '}'
- if datasource_kind == 'evm.node':
- ws_url = survey.routines.input(
- 'Enter WebSocket (wss) URL: ', value='wss://eth-mainnet.g.alchemy.com/v2'
- )
- ws_url = '${NODE_WS_URL:-' + ws_url + '}/${NODE_API_KEY:-' + api_key + '}'
- api_key = None
- else:
- api_key = '${ETHERSCAN_API_KEY:-' + api_key + '}'
-
- if datasource_kind != 'evm.etherscan':
- api_key = None
-
- if 'subsquid' in datasource_kind:
- final_url = '${SUBSQUID_URL:-' + final_url + '}'
-
- datasource: Datasource = {
- 'kind': datasource_kind,
- 'url': final_url,
- 'ws_url': ws_url,
- 'api_key': api_key,
- 'name': selected_datasource['name'],
- }
-
- datasources.append(datasource)
-
- first = True
- while ask(f'Add {'' if first else 'another '}contract?', first):
- first = False
-
- contract_name = validate_non_empty_input(
- survey.routines.input('Enter contract name: '),
- 'Contract name',
- )
- contract_address = validate_non_empty_input(
- survey.routines.input('Enter contract address: '),
- 'Contract address',
- )
-
- contract: Contract = {
- 'name': contract_name,
- 'kind': blockchain_config['contract_kind'],
- 'address': contract_address,
- 'typename': contract_name,
- }
- contracts.append(contract)
-
- if not datasources:
- return survey_config
-
- index_options = tuple(blockchain_config['indexes'].keys())
- index_comments = get_index_comments(blockchain_config['indexes'])
- index_options, index_comments = filter_index_options(index_options, index_comments, contracts, blockchain)
-
- if not index_options:
- return survey_config
-
- first = True
- while ask(f'Add {'' if first else 'another '}index?', first):
- first = False
-
- _, kind = prompt_anyof(f'Select the type of index for {blockchain}:', index_options, index_comments, 0)
-
- index_config = blockchain_config['indexes'][kind]
- index_name = validate_non_empty_input(
- survey.routines.input('Enter the index name: '),
- 'Indexer name',
- )
-
- index: Index = {
- 'name': index_name,
- 'kind': index_config['kind'],
- 'datasources': [ds['name'] for ds in datasources],
- 'handlers': None,
- 'skip_history': None,
- 'first_level': None,
- 'last_level': None,
- 'callback': None,
- 'types': None,
- 'contracts': None,
- }
-
- handlers = query_handlers([c['name'] for c in contracts], index_config.get('handler_fields'))
- index['handlers'] = handlers
-
- if 'optional_fields' in index_config:
- index.update(query_optional_fields(index_config['optional_fields'])) # type: ignore[typeddict-item]
-
- indexes.append(index)
-
- return survey_config
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index a0cb103c9..30aa1234a 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -100,7 +100,7 @@ class SqliteDatabaseConfig:
:param immune_tables: List of tables to preserve during reindexing
"""
- kind: Literal['sqlite']
+ kind: Literal['sqlite'] = 'sqlite'
path: str = DEFAULT_SQLITE_PATH
immune_tables: set[str] = Field(default_factory=set)
@@ -139,7 +139,7 @@ class PostgresDatabaseConfig:
:param connection_timeout: Connection timeout
"""
- kind: Literal['postgres']
+ kind: Literal['postgres'] = 'postgres'
host: str
user: str = DEFAULT_POSTGRES_USER
database: str = DEFAULT_POSTGRES_DATABASE
@@ -586,9 +586,7 @@ class DipDupConfig:
spec_version: ToStr
package: str
datasources: dict[str, DatasourceConfigU] = Field(default_factory=dict)
- database: SqliteDatabaseConfig | PostgresDatabaseConfig = Field(
- default_factory=lambda *a, **kw: SqliteDatabaseConfig(kind='sqlite')
- )
+ database: DatabaseConfigU = Field(default_factory=lambda *a, **kw: SqliteDatabaseConfig(kind='sqlite'))
runtimes: dict[str, RuntimeConfigU] = Field(default_factory=dict)
contracts: dict[str, ContractConfigU] = Field(default_factory=dict)
indexes: dict[str, IndexConfigU] = Field(default_factory=dict)
@@ -1162,6 +1160,7 @@ def _set_names(self) -> None:
from dipdup.config.tzip_metadata import TzipMetadataDatasourceConfig
# NOTE: Unions for Pydantic config deserialization
+DatabaseConfigU = SqliteDatabaseConfig | PostgresDatabaseConfig
RuntimeConfigU = SubstrateRuntimeConfig
ContractConfigU = EvmContractConfig | TezosContractConfig | StarknetContractConfig
DatasourceConfigU = (
diff --git a/src/dipdup/config/coinbase.py b/src/dipdup/config/coinbase.py
index fe194af3f..41e3d0cfe 100644
--- a/src/dipdup/config/coinbase.py
+++ b/src/dipdup/config/coinbase.py
@@ -21,7 +21,7 @@ class CoinbaseDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['coinbase']
+ kind: Literal['coinbase'] = 'coinbase'
api_key: str | None = None
secret_key: str | None = Field(default=None, repr=False)
passphrase: str | None = Field(default=None, repr=False)
diff --git a/src/dipdup/config/evm.py b/src/dipdup/config/evm.py
index d903937a1..9b0e4714b 100644
--- a/src/dipdup/config/evm.py
+++ b/src/dipdup/config/evm.py
@@ -55,7 +55,7 @@ class EvmContractConfig(ContractConfig):
:param typename: Alias for the contract script
"""
- kind: Literal['evm']
+ kind: Literal['evm'] = 'evm'
address: EvmAddress | None = None
abi: EvmAddress | None = None
typename: str | None = None
diff --git a/src/dipdup/config/evm_etherscan.py b/src/dipdup/config/evm_etherscan.py
index 2ff8ba722..8e24df9d9 100644
--- a/src/dipdup/config/evm_etherscan.py
+++ b/src/dipdup/config/evm_etherscan.py
@@ -23,7 +23,7 @@ class EvmEtherscanDatasourceConfig(DatasourceConfig):
"""
# NOTE: Alias, remove in 9.0
- kind: Literal['evm.etherscan'] | Literal['abi.etherscan']
+ kind: Literal['evm.etherscan'] | Literal['abi.etherscan'] = 'evm.etherscan'
url: str
api_key: str | None = None
diff --git a/src/dipdup/config/evm_events.py b/src/dipdup/config/evm_events.py
index aedb92fe3..f277227c1 100644
--- a/src/dipdup/config/evm_events.py
+++ b/src/dipdup/config/evm_events.py
@@ -61,7 +61,7 @@ class EvmEventsIndexConfig(EvmIndexConfig):
:param last_level: Level to stop indexing and disable this index
"""
- kind: Literal['evm.events']
+ kind: Literal['evm.events'] = 'evm.events'
datasources: tuple[Alias[EvmDatasourceConfigU], ...]
handlers: tuple[EvmEventsHandlerConfig, ...]
diff --git a/src/dipdup/config/evm_node.py b/src/dipdup/config/evm_node.py
index 9ee1eb170..fe7a0ec95 100644
--- a/src/dipdup/config/evm_node.py
+++ b/src/dipdup/config/evm_node.py
@@ -22,7 +22,7 @@ class EvmNodeDatasourceConfig(DatasourceConfig):
:param rollback_depth: A number of blocks to store in database for rollback
"""
- kind: Literal['evm.node']
+ kind: Literal['evm.node'] = 'evm.node'
url: Url
ws_url: WsUrl | None = None
http: HttpConfig | None = None
diff --git a/src/dipdup/config/evm_subsquid.py b/src/dipdup/config/evm_subsquid.py
index 9ce1ac9a4..c8848480e 100644
--- a/src/dipdup/config/evm_subsquid.py
+++ b/src/dipdup/config/evm_subsquid.py
@@ -19,7 +19,7 @@ class EvmSubsquidDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['evm.subsquid']
+ kind: Literal['evm.subsquid'] = 'evm.subsquid'
url: Url
http: HttpConfig | None = None
diff --git a/src/dipdup/config/evm_transactions.py b/src/dipdup/config/evm_transactions.py
index 956685206..3fdeb06c7 100644
--- a/src/dipdup/config/evm_transactions.py
+++ b/src/dipdup/config/evm_transactions.py
@@ -93,7 +93,7 @@ class EvmTransactionsIndexConfig(EvmIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['evm.transactions']
+ kind: Literal['evm.transactions'] = 'evm.transactions'
datasources: tuple[Alias[EvmDatasourceConfigU], ...]
handlers: tuple[EvmTransactionsHandlerConfig, ...]
diff --git a/src/dipdup/config/http.py b/src/dipdup/config/http.py
index 6b3cfcf0c..b4341e90e 100644
--- a/src/dipdup/config/http.py
+++ b/src/dipdup/config/http.py
@@ -18,6 +18,6 @@ class HttpDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['http']
+ kind: Literal['http'] = 'http'
url: str
http: HttpConfig | None = None
diff --git a/src/dipdup/config/ipfs.py b/src/dipdup/config/ipfs.py
index 148ce6df1..90993b647 100644
--- a/src/dipdup/config/ipfs.py
+++ b/src/dipdup/config/ipfs.py
@@ -20,6 +20,6 @@ class IpfsDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['ipfs']
+ kind: Literal['ipfs'] = 'ipfs'
url: str = DEFAULT_IPFS_URL
http: HttpConfig | None = None
diff --git a/src/dipdup/config/starknet.py b/src/dipdup/config/starknet.py
index 52469d5a9..8e026c70d 100644
--- a/src/dipdup/config/starknet.py
+++ b/src/dipdup/config/starknet.py
@@ -65,7 +65,7 @@ class StarknetContractConfig(ContractConfig):
:param typename: Alias for the contract script
"""
- kind: Literal['starknet']
+ kind: Literal['starknet'] = 'starknet'
address: StarknetAddress | None = None
abi: StarknetAddress | None = None
typename: str | None = None
diff --git a/src/dipdup/config/starknet_events.py b/src/dipdup/config/starknet_events.py
index e09b69392..14e7cf901 100644
--- a/src/dipdup/config/starknet_events.py
+++ b/src/dipdup/config/starknet_events.py
@@ -61,7 +61,7 @@ class StarknetEventsIndexConfig(StarknetIndexConfig):
"""
- kind: Literal['starknet.events']
+ kind: Literal['starknet.events'] = 'starknet.events'
datasources: tuple[Alias[StarknetDatasourceConfigU], ...]
handlers: tuple[StarknetEventsHandlerConfig, ...]
diff --git a/src/dipdup/config/starknet_node.py b/src/dipdup/config/starknet_node.py
index 5e2b72251..54318a36a 100644
--- a/src/dipdup/config/starknet_node.py
+++ b/src/dipdup/config/starknet_node.py
@@ -22,7 +22,7 @@ class StarknetNodeDatasourceConfig(DatasourceConfig):
:param rollback_depth: A number of blocks to store in database for rollback
"""
- kind: Literal['starknet.node']
+ kind: Literal['starknet.node'] = 'starknet.node'
url: Url
ws_url: WsUrl | None = None
http: HttpConfig | None = None
diff --git a/src/dipdup/config/starknet_subsquid.py b/src/dipdup/config/starknet_subsquid.py
index 581f8400d..6b270ec7a 100644
--- a/src/dipdup/config/starknet_subsquid.py
+++ b/src/dipdup/config/starknet_subsquid.py
@@ -19,7 +19,7 @@ class StarknetSubsquidDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['starknet.subsquid']
+ kind: Literal['starknet.subsquid'] = 'starknet.subsquid'
url: Url
http: HttpConfig | None = None
diff --git a/src/dipdup/config/substrate_events.py b/src/dipdup/config/substrate_events.py
index c814fe712..5e008b656 100644
--- a/src/dipdup/config/substrate_events.py
+++ b/src/dipdup/config/substrate_events.py
@@ -62,7 +62,7 @@ class SubstrateEventsIndexConfig(SubstrateIndexConfig):
:param runtime: Substrate runtime
"""
- kind: Literal['substrate.events']
+ kind: Literal['substrate.events'] = 'substrate.events'
datasources: tuple[Alias[SubstrateDatasourceConfigU], ...]
handlers: tuple[SubstrateEventsHandlerConfig, ...]
runtime: Alias[SubstrateRuntimeConfig]
diff --git a/src/dipdup/config/substrate_node.py b/src/dipdup/config/substrate_node.py
index a7eeb402c..a0bd09f4f 100644
--- a/src/dipdup/config/substrate_node.py
+++ b/src/dipdup/config/substrate_node.py
@@ -21,7 +21,7 @@ class SubstrateNodeDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['substrate.node']
+ kind: Literal['substrate.node'] = 'substrate.node'
url: Url
ws_url: WsUrl | None = None
http: HttpConfig | None = None
diff --git a/src/dipdup/config/substrate_subscan.py b/src/dipdup/config/substrate_subscan.py
index 30dc2d178..8167da3b4 100644
--- a/src/dipdup/config/substrate_subscan.py
+++ b/src/dipdup/config/substrate_subscan.py
@@ -19,7 +19,7 @@ class SubstrateSubscanDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['substrate.subscan']
+ kind: Literal['substrate.subscan'] = 'substrate.subscan'
url: str
api_key: str | None = None
diff --git a/src/dipdup/config/substrate_subsquid.py b/src/dipdup/config/substrate_subsquid.py
index b2de5eb3e..777ae1880 100644
--- a/src/dipdup/config/substrate_subsquid.py
+++ b/src/dipdup/config/substrate_subsquid.py
@@ -19,7 +19,7 @@ class SubstrateSubsquidDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['substrate.subsquid']
+ kind: Literal['substrate.subsquid'] = 'substrate.subsquid'
url: Url
http: HttpConfig | None = None
diff --git a/src/dipdup/config/tezos.py b/src/dipdup/config/tezos.py
index 707cfe5a5..83e3490cf 100644
--- a/src/dipdup/config/tezos.py
+++ b/src/dipdup/config/tezos.py
@@ -59,7 +59,7 @@ class TezosContractConfig(ContractConfig):
:param typename: Alias for the contract script
"""
- kind: Literal['tezos']
+ kind: Literal['tezos'] = 'tezos'
address: TezosAddress | None = None
code_hash: int | TezosAddress | None = None
typename: str | None = None
diff --git a/src/dipdup/config/tezos_big_maps.py b/src/dipdup/config/tezos_big_maps.py
index 0777a1ea3..d09e664d6 100644
--- a/src/dipdup/config/tezos_big_maps.py
+++ b/src/dipdup/config/tezos_big_maps.py
@@ -79,7 +79,7 @@ class TezosBigMapsIndexConfig(TezosIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['tezos.big_maps']
+ kind: Literal['tezos.big_maps'] = 'tezos.big_maps'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
handlers: tuple[TezosBigMapsHandlerConfig, ...]
diff --git a/src/dipdup/config/tezos_events.py b/src/dipdup/config/tezos_events.py
index 1c39d4d3d..a38bb394c 100644
--- a/src/dipdup/config/tezos_events.py
+++ b/src/dipdup/config/tezos_events.py
@@ -83,7 +83,7 @@ class TezosEventsIndexConfig(TezosIndexConfig):
:param last_level: Last block level to index
"""
- kind: Literal['tezos.events']
+ kind: Literal['tezos.events'] = 'tezos.events'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
handlers: tuple[TezosEventsHandlerConfigU, ...]
diff --git a/src/dipdup/config/tezos_head.py b/src/dipdup/config/tezos_head.py
index f727a2d2d..c76b57ecc 100644
--- a/src/dipdup/config/tezos_head.py
+++ b/src/dipdup/config/tezos_head.py
@@ -41,7 +41,7 @@ class TezosHeadIndexConfig(TezosIndexConfig):
:param datasources: `tezos` datasources to use
"""
- kind: Literal['tezos.head']
+ kind: Literal['tezos.head'] = 'tezos.head'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
callback: str
diff --git a/src/dipdup/config/tezos_operations.py b/src/dipdup/config/tezos_operations.py
index 702282d28..e6fa68bfe 100644
--- a/src/dipdup/config/tezos_operations.py
+++ b/src/dipdup/config/tezos_operations.py
@@ -306,7 +306,7 @@ class TezosOperationsIndexConfig(TezosIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['tezos.operations']
+ kind: Literal['tezos.operations'] = 'tezos.operations'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
handlers: tuple[TezosOperationsHandlerConfig, ...]
contracts: list[Alias[TezosContractConfig]] = Field(default_factory=list)
@@ -431,7 +431,7 @@ class TezosOperationsUnfilteredIndexConfig(TezosIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['tezos.operations_unfiltered']
+ kind: Literal['tezos.operations_unfiltered'] = 'tezos.operations_unfiltered'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
callback: str
types: tuple[TezosOperationType, ...] = (TezosOperationType.transaction,)
diff --git a/src/dipdup/config/tezos_token_balances.py b/src/dipdup/config/tezos_token_balances.py
index 3dbdbe0f4..ecd7667ed 100644
--- a/src/dipdup/config/tezos_token_balances.py
+++ b/src/dipdup/config/tezos_token_balances.py
@@ -56,7 +56,7 @@ class TezosTokenBalancesIndexConfig(TezosIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['tezos.token_balances']
+ kind: Literal['tezos.token_balances'] = 'tezos.token_balances'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
handlers: tuple[TezosTokenBalancesHandlerConfig, ...]
diff --git a/src/dipdup/config/tezos_token_transfers.py b/src/dipdup/config/tezos_token_transfers.py
index bd76a29b7..caf00e227 100644
--- a/src/dipdup/config/tezos_token_transfers.py
+++ b/src/dipdup/config/tezos_token_transfers.py
@@ -59,7 +59,7 @@ class TezosTokenTransfersIndexConfig(TezosIndexConfig):
:param last_level: Level to stop indexing at
"""
- kind: Literal['tezos.token_transfers']
+ kind: Literal['tezos.token_transfers'] = 'tezos.token_transfers'
datasources: tuple[Alias[TezosTzktDatasourceConfig], ...]
handlers: tuple[TezosTokenTransfersHandlerConfig, ...]
diff --git a/src/dipdup/config/tezos_tzkt.py b/src/dipdup/config/tezos_tzkt.py
index 9eba47d18..564fe414a 100644
--- a/src/dipdup/config/tezos_tzkt.py
+++ b/src/dipdup/config/tezos_tzkt.py
@@ -34,7 +34,7 @@ class TezosTzktDatasourceConfig(DatasourceConfig):
:param rollback_depth: Number of blocks to keep in the database to handle reorgs
"""
- kind: Literal['tezos.tzkt']
+ kind: Literal['tezos.tzkt'] = 'tezos.tzkt'
url: Url = DEFAULT_TZKT_URL
http: HttpConfig | None = None
buffer_size: int = 0
diff --git a/src/dipdup/config/tzip_metadata.py b/src/dipdup/config/tzip_metadata.py
index 1a26ab5e7..b787fde43 100644
--- a/src/dipdup/config/tzip_metadata.py
+++ b/src/dipdup/config/tzip_metadata.py
@@ -22,7 +22,7 @@ class TzipMetadataDatasourceConfig(DatasourceConfig):
:param http: HTTP client configuration
"""
- kind: Literal['tzip_metadata']
+ kind: Literal['tzip_metadata'] = 'tzip_metadata'
network: TzipMetadataNetwork
url: str = DEFAULT_TZIP_METADATA_URL
http: HttpConfig | None = None
diff --git a/src/dipdup/datasources/starknet_node.py b/src/dipdup/datasources/starknet_node.py
index f05d53108..d210ea430 100644
--- a/src/dipdup/datasources/starknet_node.py
+++ b/src/dipdup/datasources/starknet_node.py
@@ -31,7 +31,9 @@ class StarknetNodeDatasource(IndexDatasource[StarknetNodeDatasourceConfig]):
def __init__(self, config: StarknetNodeDatasourceConfig, merge_subscriptions: bool = False) -> None:
super().__init__(config, merge_subscriptions)
self._starknetpy: StarknetpyClient | None = None
- self._block_cache: LRU[int, StarknetBlockWithTxHashes | PendingStarknetBlockWithTxHashes] = LRU(BLOCK_CACHE_SIZE)
+ self._block_cache: LRU[int, StarknetBlockWithTxHashes | PendingStarknetBlockWithTxHashes] = LRU(
+ BLOCK_CACHE_SIZE
+ )
@property
def starknetpy(self) -> 'StarknetpyClient':
@@ -82,7 +84,9 @@ async def get_events(
continuation_token=continuation_token,
)
- async def get_block_with_tx_hashes(self, block_hash: int) -> Union['StarknetBlockWithTxHashes', 'PendingStarknetBlockWithTxHashes']:
+ async def get_block_with_tx_hashes(
+ self, block_hash: int
+ ) -> Union['StarknetBlockWithTxHashes', 'PendingStarknetBlockWithTxHashes']:
if block := self._block_cache.get(block_hash, None):
return block
diff --git a/src/dipdup/indexes/starknet_events/fetcher.py b/src/dipdup/indexes/starknet_events/fetcher.py
index ddc7e432b..d23e8893b 100644
--- a/src/dipdup/indexes/starknet_events/fetcher.py
+++ b/src/dipdup/indexes/starknet_events/fetcher.py
@@ -55,7 +55,7 @@ class EventFetcherChannel(FetcherChannel[StarknetEventData, StarknetNodeDatasour
async def fetch(self) -> None:
address, key0s = next(iter(self._filter))
-
+
datasource = self._datasources[0]
events_chunk = await datasource.get_events(
@@ -67,9 +67,9 @@ async def fetch(self) -> None:
)
for event in events_chunk.events:
- # NOTE:
+ # NOTE:
if event.block_hash is None or event.transaction_hash is None:
- _logger.info("Skipping event. No block_hash or transaction_hash found in %s", event)
+ _logger.info('Skipping event. No block_hash or transaction_hash found in %s', event)
continue
block = await datasource.get_block_with_tx_hashes(
@@ -77,7 +77,7 @@ async def fetch(self) -> None:
)
if block is None:
- _logger.info("Skipping event. No block exists for block_hash. BlackHash=%s", event.block_hash)
+ _logger.info('Skipping event. No block exists for block_hash. BlackHash=%s', event.block_hash)
continue
timestamp = block.timestamp
@@ -85,7 +85,7 @@ async def fetch(self) -> None:
# NOTE: This event is corrupt, possibly due to old age.
if transaction_idx < 0:
- _logger.info("Skipping event. No transaction_hash exists in block. TxHash=%s", event.transaction_hash)
+ _logger.info('Skipping event. No transaction_hash exists in block. TxHash=%s', event.transaction_hash)
continue
self._buffer[event.block_number].append( # type: ignore[index]
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index 5b983f668..91b68e9d3 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -3,21 +3,26 @@
Ask user some question with Click; render Jinja2 templates with answers.
"""
+import dataclasses
import logging
import re
+from itertools import chain
from pathlib import Path
+from typing import Any
+from typing import get_args
from pydantic import ConfigDict
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass
+from pydantic.fields import FieldInfo
from typing_extensions import TypedDict
from dipdup import __version__
-from dipdup._survey import DipDupSurveyConfig
-from dipdup._survey import prompt_anyof
-from dipdup._survey import query_survey_config
from dipdup.cli import big_yellow_echo
from dipdup.cli import echo
+from dipdup.config import DatabaseConfigU
+from dipdup.config import DatasourceConfigU
+from dipdup.config import RuntimeConfigU
from dipdup.config import ToStr
from dipdup.env import get_package_path
from dipdup.env import get_pyproject_name
@@ -80,7 +85,7 @@ class Answers(TypedDict):
hasura_image: str
line_length: ToStr
package_manager: str
- _survey_config: DipDupSurveyConfig | None
+ _survey_config: dict[str, Any] | None
def get_default_answers() -> Answers:
@@ -125,21 +130,22 @@ def get_replay_path(name: str) -> Path:
return Path(__file__).parent / 'projects' / name / 'replay.yaml'
-def template_from_terminal() -> tuple[str | None, DipDupSurveyConfig | None]:
- _, mode = prompt_anyof(
- question='How would you like to set up your new DipDup project?',
- options=(
- 'From template',
- 'Interactively',
- 'Blank',
- ),
- comments=(
- 'Use one of demo projects',
- 'Guided setup with prompts',
- 'Begin with an empty project',
- ),
- default=0,
- )
+def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
+ # _, mode = prompt_anyof(
+ # question='How would you like to set up your new DipDup project?',
+ # options=(
+ # 'From template',
+ # 'Interactively',
+ # 'Blank',
+ # ),
+ # comments=(
+ # 'Use one of demo projects',
+ # 'Guided setup with prompts',
+ # 'Begin with an empty project',
+ # ),
+ # default=0,
+ # )
+ mode = 'Interactively'
if mode == 'Blank':
return ('demo_blank', None)
@@ -151,25 +157,28 @@ def template_from_terminal() -> tuple[str | None, DipDupSurveyConfig | None]:
'Starknet',
'Substrate',
'Tezos',
+ '[multiple]',
),
comments=(
- 'EVM-compatible blockchains',
+ 'EVM-compatible',
'Starknet',
'Substrate',
'Tezos',
+ 'Disable filtering',
),
default=0,
)
- blockchain = res[1].lower()
+ namespace = res[1].lower() if res[1] != '[multiple]' else None
if mode == 'Interactively':
replay_path = get_replay_path('demo_blank')
- survey_config = query_survey_config(blockchain)
+ survey_config = query_survey_config(namespace)
return ('demo_blank', survey_config)
if mode == 'From template':
options, comments = [], []
- for name in TEMPLATES[blockchain]:
+ templates = TEMPLATES[namespace] if namespace else chain(v for v in TEMPLATES.values())
+ for name in templates:
replay_path = get_replay_path(name)
_answers = answers_from_replay(replay_path)
options.append(_answers['template'])
@@ -409,3 +418,102 @@ def _render(answers: Answers, template_path: Path, output_path: Path, force: boo
)
write(output_path, content, overwrite=force)
+
+
+def fill_config_from_input(
+ config: dict[str, Any],
+ section: str,
+ section_kinds: tuple[type],
+ filter: str,
+) -> None:
+ section_dict = config.get(section, {})
+ another = False
+
+ print(f'Configuring `{section}` section')
+
+ while True:
+ add_entity = input(f'Do you want to add{' another' if another else ''} one? (yes/no): ').strip().lower()
+ if add_entity != 'yes':
+ break
+
+ # Ask for the entity name
+ if section == 'database':
+ name = ''
+ else:
+ name = input('Enter a name for the entity: ').strip()
+
+ # Ask for the type of the entity
+ print('Available entity types:')
+ for i, entity_type in enumerate(section_kinds, start=1):
+ kind = entity_type.__dataclass_fields__['kind'].default
+ assert kind
+
+ if filter and '.' in kind and not kind.startswith(filter):
+ continue
+
+ print(f'{i}. {kind}')
+
+ type_choice = int(input('Choose an entity type (number): ').strip()) - 1
+ # entity_type_name = list(section_kinds)[type_choice]
+ entity_model = section_kinds[type_choice]
+
+ # Gather input for the fields of the chosen type
+ entity_data = {}
+ for field_name, field in entity_model.__dataclass_fields__.items():
+ if field_name in {'kind', 'http'}:
+ continue
+
+ default = field.default
+ if default is dataclasses.MISSING:
+ default = None
+ elif isinstance(default, FieldInfo):
+ # else:
+ # print(default.__dict__)
+ default = default.default_factory()
+
+ field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
+ entity_data[field_name] = field_value or default
+
+ # Validate and add the entity
+ try:
+ entity_instance = entity_model(**entity_data)
+ section_dict[name] = entity_instance.__dict__
+ another = True
+ except Exception as e:
+ print(f'Error: {e}. Please try again.')
+
+
+def prompt_anyof(
+ question: str,
+ options: tuple[str, ...],
+ comments: tuple[str, ...],
+ default: int,
+) -> tuple[int, str]:
+ """Ask user to choose one of the options; returns index and value"""
+ import survey
+ from tabulate import tabulate
+
+ table = tabulate(
+ zip(options, comments, strict=False),
+ tablefmt='plain',
+ )
+ index = survey.routines.select(
+ question + '\n',
+ options=table.split('\n'),
+ index=default,
+ )
+ return index, options[index]
+
+
+def query_survey_config(namespace: str | None) -> dict[str, Any]:
+ config_dict = {}
+
+ # NOTE: It's not a config section; we'll handle it later
+ fill_config_from_input(config_dict, 'database', get_args(DatabaseConfigU), namespace)
+
+ if namespace == 'substrate':
+ fill_config_from_input(config_dict, 'runtimes', get_args(RuntimeConfigU), namespace)
+
+ fill_config_from_input(config_dict, 'datasources', get_args(DatasourceConfigU), namespace)
+
+ return config_dict
From e38a71e6bd66818f549ab40e9b74239318c26bd4 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 13 Feb 2025 19:58:22 -0300
Subject: [PATCH 02/30] almost there
---
src/dipdup/cli.py | 21 +++++-
src/dipdup/config/__init__.py | 2 +-
src/dipdup/project.py | 116 ++++++++++++++++++++--------------
src/dipdup/yaml.py | 2 +-
4 files changed, 91 insertions(+), 50 deletions(-)
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 6eccc226a..3b8bd4761 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -27,6 +27,7 @@
from dipdup.install import EPILOG
from dipdup.install import WELCOME_ASCII
from dipdup.sys import set_up_process
+from dipdup.yaml import DipDupYAMLConfig
if TYPE_CHECKING:
from dipdup.config import DipDupConfig
@@ -849,6 +850,7 @@ async def new(
from dipdup.project import answers_from_terminal
from dipdup.project import get_default_answers
from dipdup.project import render_project
+ from dipdup.project import template_from_terminal
if quiet:
answers = get_default_answers()
@@ -860,13 +862,30 @@ async def new(
answers['template'] = template
else:
try:
- answers = answers_from_terminal(template)
+ answers = answers_from_terminal()
+ answers['template'] = template or 'demo_blank'
+
+ if template:
+ echo(f'Using template `{template}`\n')
+ config_dict = {}
+ else:
+ template, config_dict = template_from_terminal()
+
except Escape:
return
_logger.info('Rendering project')
render_project(answers, force)
+ if config_dict:
+ config_dict['package'] = answers['package']
+ config_dict['spec_version'] = '3.0'
+
+ config = DipDupYAMLConfig(**config_dict)
+
+ path = env.get_package_path(config['package']) / ROOT_CONFIG
+ path.write_text(config.dump())
+
_logger.info('Initializing project')
config = DipDupConfig.load([Path(answers['package'])])
config.initialize()
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 30aa1234a..0020e5b8c 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -316,7 +316,7 @@ class IndexTemplateConfig(NameMixin):
"""
- kind = 'template'
+ kind: Literal['template'] = 'template'
template: str
values: dict[str, Any]
first_level: int = 0
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index 91b68e9d3..b97342268 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -20,8 +20,10 @@
from dipdup import __version__
from dipdup.cli import big_yellow_echo
from dipdup.cli import echo
-from dipdup.config import DatabaseConfigU
from dipdup.config import DatasourceConfigU
+from dipdup.config import HookConfig
+from dipdup.config import IndexConfigU
+from dipdup.config import JobConfig
from dipdup.config import RuntimeConfigU
from dipdup.config import ToStr
from dipdup.env import get_package_path
@@ -34,6 +36,12 @@
CODEGEN_HEADER = f'# generated by DipDup {__version__.split("+")[0]}'
+SINGULAR_FORMS = {
+ 'datasources': 'datasource',
+ 'indexes': 'index',
+ 'hooks': 'hook',
+ 'jobs': 'job',
+}
# NOTE: All templates are stored in src/dipdup/projects
TEMPLATES: dict[str, tuple[str, ...]] = {
@@ -85,7 +93,6 @@ class Answers(TypedDict):
hasura_image: str
line_length: ToStr
package_manager: str
- _survey_config: dict[str, Any] | None
def get_default_answers() -> Answers:
@@ -103,7 +110,6 @@ def get_default_answers() -> Answers:
hasura_image='hasura/graphql-engine:latest',
line_length='120',
package_manager='uv',
- _survey_config=None,
)
@@ -131,21 +137,20 @@ def get_replay_path(name: str) -> Path:
def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
- # _, mode = prompt_anyof(
- # question='How would you like to set up your new DipDup project?',
- # options=(
- # 'From template',
- # 'Interactively',
- # 'Blank',
- # ),
- # comments=(
- # 'Use one of demo projects',
- # 'Guided setup with prompts',
- # 'Begin with an empty project',
- # ),
- # default=0,
- # )
- mode = 'Interactively'
+ _, mode = prompt_anyof(
+ question='How would you like to set up your new DipDup project?',
+ options=(
+ 'Interactively',
+ 'From template',
+ 'Blank',
+ ),
+ comments=(
+ 'Guided setup with prompts',
+ 'Use one of demo projects',
+ 'Begin with an empty project',
+ ),
+ default=0,
+ )
if mode == 'Blank':
return ('demo_blank', None)
@@ -164,7 +169,7 @@ def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
'Starknet',
'Substrate',
'Tezos',
- 'Disable filtering',
+ '(disable filtering)',
),
default=0,
)
@@ -172,8 +177,8 @@ def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
if mode == 'Interactively':
replay_path = get_replay_path('demo_blank')
- survey_config = query_survey_config(namespace)
- return ('demo_blank', survey_config)
+ config_dict = config_from_terminal(namespace)
+ return ('demo_blank', config_dict)
if mode == 'From template':
options, comments = [], []
@@ -195,7 +200,7 @@ def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
raise NotImplementedError
-def answers_from_terminal(template: str | None) -> Answers:
+def answers_from_terminal() -> Answers:
"""Script running on dipdup new command and will create a new project base from interactive survey"""
import survey # type: ignore[import-untyped]
@@ -206,14 +211,6 @@ def answers_from_terminal(template: str | None) -> Answers:
answers = get_default_answers()
- if template:
- echo(f'Using template `{template}`\n')
- else:
- template, survey_config = template_from_terminal()
- answers['_survey_config'] = survey_config
-
- answers['template'] = template or 'demo_blank'
-
big_yellow_echo('Set up project')
while True:
@@ -301,6 +298,7 @@ def answers_from_terminal(template: str | None) -> Answers:
'Enter maximum line length for linters: ',
value=answers['line_length'],
)
+
return answers
@@ -424,28 +422,31 @@ def fill_config_from_input(
config: dict[str, Any],
section: str,
section_kinds: tuple[type],
- filter: str,
+ filter: str | None,
) -> None:
section_dict = config.get(section, {})
another = False
- print(f'Configuring `{section}` section')
+ print(f'Configuring `{section}` section \n')
while True:
- add_entity = input(f'Do you want to add{' another' if another else ''} one? (yes/no): ').strip().lower()
+ entity_singular = SINGULAR_FORMS[section]
+ another_str = 'another' if another else 'the first'
+ add_entity = input(f'Do you want to add {another_str} {entity_singular}? (yes/no): ').strip().lower()
if add_entity != 'yes':
break
# Ask for the entity name
- if section == 'database':
- name = ''
- else:
- name = input('Enter a name for the entity: ').strip()
+ name = input(f'Enter {entity_singular} name: ').strip()
# Ask for the type of the entity
- print('Available entity types:')
+ print(f'Available {entity_singular} kinds:')
for i, entity_type in enumerate(section_kinds, start=1):
- kind = entity_type.__dataclass_fields__['kind'].default
+ try:
+ kind = entity_type.__dataclass_fields__['kind'].default
+ except KeyError:
+ kind = entity_type.__name__
+
assert kind
if filter and '.' in kind and not kind.startswith(filter):
@@ -453,8 +454,11 @@ def fill_config_from_input(
print(f'{i}. {kind}')
- type_choice = int(input('Choose an entity type (number): ').strip()) - 1
- # entity_type_name = list(section_kinds)[type_choice]
+ if i == 1:
+ type_choice = 0
+ else:
+ type_choice = int(input(f'Choose {entity_singular} type (number): ').strip()) - 1
+
entity_model = section_kinds[type_choice]
# Gather input for the fields of the chosen type
@@ -462,9 +466,14 @@ def fill_config_from_input(
for field_name, field in entity_model.__dataclass_fields__.items():
if field_name in {'kind', 'http'}:
continue
+ if field_name.startswith('_'):
+ continue
+ if field_name == 'handlers':
+ entity_data[field_name] = ()
+ continue
default = field.default
- if default is dataclasses.MISSING:
+ if default == dataclasses.MISSING:
default = None
elif isinstance(default, FieldInfo):
# else:
@@ -472,6 +481,14 @@ def fill_config_from_input(
default = default.default_factory()
field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
+ if field_value == '':
+ field_value = default
+
+ # NOTE: Basic transformations: string lists
+ if isinstance(field_value, str):
+ if ',' in field_value:
+ field_value = field_value.split(',')
+
entity_data[field_name] = field_value or default
# Validate and add the entity
@@ -482,6 +499,10 @@ def fill_config_from_input(
except Exception as e:
print(f'Error: {e}. Please try again.')
+ print(section_dict)
+
+ config[section] = section_dict
+
def prompt_anyof(
question: str,
@@ -505,15 +526,16 @@ def prompt_anyof(
return index, options[index]
-def query_survey_config(namespace: str | None) -> dict[str, Any]:
+def config_from_terminal(namespace: str | None) -> dict[str, Any]:
config_dict = {}
- # NOTE: It's not a config section; we'll handle it later
- fill_config_from_input(config_dict, 'database', get_args(DatabaseConfigU), namespace)
-
- if namespace == 'substrate':
+ # NOTE: Substrate or multichain
+ if namespace in {'substrate', None}:
fill_config_from_input(config_dict, 'runtimes', get_args(RuntimeConfigU), namespace)
fill_config_from_input(config_dict, 'datasources', get_args(DatasourceConfigU), namespace)
+ fill_config_from_input(config_dict, 'indexes', get_args(IndexConfigU), namespace)
+ fill_config_from_input(config_dict, 'hooks', (HookConfig,), None)
+ fill_config_from_input(config_dict, 'jobs', (JobConfig,), None)
return config_dict
diff --git a/src/dipdup/yaml.py b/src/dipdup/yaml.py
index 771c20747..3f30fb710 100644
--- a/src/dipdup/yaml.py
+++ b/src/dipdup/yaml.py
@@ -45,7 +45,7 @@ def exclude_none(config_json: Any) -> Any:
if isinstance(config_json, list | tuple):
return [exclude_none(i) for i in config_json if i is not None]
if isinstance(config_json, dict):
- return {k: exclude_none(v) for k, v in config_json.items() if v is not None}
+ return {k: exclude_none(v) for k, v in config_json.items() if v is not None and not k.startswith('_')}
return config_json
From 35efc9c5b7111792dda8a995d2de610bc77992a2 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 15 Feb 2025 15:12:24 -0300
Subject: [PATCH 03/30] another iteration
---
CHANGELOG.md | 10 +++++
src/dipdup/cli.py | 8 +++-
src/dipdup/config/coinbase.py | 4 ++
src/dipdup/datasources/coinbase.py | 1 -
src/dipdup/project.py | 63 +++++++++++++++++++++++-------
5 files changed, 69 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e4b01189..2c9973223 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
Releases prior to 7.0 has been removed from this file to declutter search results; see the [archived copy](https://github.com/dipdup-io/dipdup/blob/8.0.0b5/CHANGELOG.md) for the full list.
+## [Unreleased]
+
+### Added
+
+- cli: Rewritten interactive mode for `new` command.
+
+### Fixed
+
+- coinbase: Fixed crash when using coinbase datasource.
+
## [8.2.0] - 2025-02-10
### Added
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 3b8bd4761..c1459b81f 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -878,8 +878,12 @@ async def new(
render_project(answers, force)
if config_dict:
- config_dict['package'] = answers['package']
- config_dict['spec_version'] = '3.0'
+ # NOTE: Preserve the header at the top of the file
+ config_dict = {
+ 'package': answers['package'],
+ 'spec_version': '3.0',
+ **config_dict,
+ }
config = DipDupYAMLConfig(**config_dict)
diff --git a/src/dipdup/config/coinbase.py b/src/dipdup/config/coinbase.py
index 41e3d0cfe..c6b8a8f88 100644
--- a/src/dipdup/config/coinbase.py
+++ b/src/dipdup/config/coinbase.py
@@ -27,3 +27,7 @@ class CoinbaseDatasourceConfig(DatasourceConfig):
passphrase: str | None = Field(default=None, repr=False)
http: HttpConfig | None = None
+
+ @property
+ def url(self) -> str:
+ return 'https://api.pro.coinbase.com'
diff --git a/src/dipdup/datasources/coinbase.py b/src/dipdup/datasources/coinbase.py
index 2871fd23e..346fe1087 100644
--- a/src/dipdup/datasources/coinbase.py
+++ b/src/dipdup/datasources/coinbase.py
@@ -11,7 +11,6 @@
from dipdup.models.coinbase import CoinbaseCandleInterval
CANDLES_REQUEST_LIMIT = 300
-API_URL = 'https://api.pro.coinbase.com'
class CoinbaseDatasource(Datasource[CoinbaseDatasourceConfig]):
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index b97342268..adc891660 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -38,9 +38,11 @@
SINGULAR_FORMS = {
'datasources': 'datasource',
+ 'contracts': 'contract',
'indexes': 'index',
'hooks': 'hook',
'jobs': 'job',
+ 'runtimes': 'runtime',
}
# NOTE: All templates are stored in src/dipdup/projects
@@ -424,6 +426,8 @@ def fill_config_from_input(
section_kinds: tuple[type],
filter: str | None,
) -> None:
+ import survey
+
section_dict = config.get(section, {})
another = False
@@ -431,17 +435,37 @@ def fill_config_from_input(
while True:
entity_singular = SINGULAR_FORMS[section]
- another_str = 'another' if another else 'the first'
- add_entity = input(f'Do you want to add {another_str} {entity_singular}? (yes/no): ').strip().lower()
+
+ if another:
+ another_str = 'another'
+ choices = ('no', 'yes')
+ else:
+ another_str = 'the first'
+ choices = (
+ 'yes',
+ 'no',
+ )
+
+ _, add_entity = prompt_anyof(
+ f'Do you want to add {another_str} {entity_singular}?',
+ choices,
+ ('', ''),
+ default=0,
+ )
if add_entity != 'yes':
break
- # Ask for the entity name
- name = input(f'Enter {entity_singular} name: ').strip()
+ # NOTE: All sections are mappings alias to dict
+ name = survey.routines.input(
+ f'Enter {entity_singular} name: ',
+ )
+
+ # NOTE: Prepare a filtered (or not) list of entity types
# Ask for the type of the entity
print(f'Available {entity_singular} kinds:')
- for i, entity_type in enumerate(section_kinds, start=1):
+ matched = {}
+ for entity_type in section_kinds:
try:
kind = entity_type.__dataclass_fields__['kind'].default
except KeyError:
@@ -452,14 +476,23 @@ def fill_config_from_input(
if filter and '.' in kind and not kind.startswith(filter):
continue
- print(f'{i}. {kind}')
+ matched[kind] = entity_type
+
+ if len(matched) != 1:
+ kinds = sorted(matched.keys())
+ comments = tuple('' for _ in kinds)
+ print(f'Available {entity_singular} kinds: {kinds} {comments}')
- if i == 1:
- type_choice = 0
+ _, kind = prompt_anyof(
+ f'Choose {entity_singular} kind: ',
+ kinds,
+ comments,
+ default=0,
+ )
else:
- type_choice = int(input(f'Choose {entity_singular} type (number): ').strip()) - 1
+ kind = next(iter(matched))
- entity_model = section_kinds[type_choice]
+ entity_model = matched[kind]
# Gather input for the fields of the chosen type
entity_data = {}
@@ -476,11 +509,13 @@ def fill_config_from_input(
if default == dataclasses.MISSING:
default = None
elif isinstance(default, FieldInfo):
- # else:
- # print(default.__dict__)
- default = default.default_factory()
+ default = default.default_factory() if default.default_factory else default.default
- field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
+ # field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
+ field_value = survey.routines.input(
+ f'Enter value for `{field_name}` [{field.type}]: ',
+ value=default or '',
+ )
if field_value == '':
field_value = default
From a4c7da3c39a02b677e558bbdd7cae44b7ceaa82a Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 15 Feb 2025 16:15:59 -0300
Subject: [PATCH 04/30] wip
---
src/dipdup/cli.py | 2 +-
src/dipdup/config/__init__.py | 29 +++++++++++-
src/dipdup/config/_mixin.py | 15 +++++++
src/dipdup/config/coinbase.py | 2 +-
src/dipdup/project.py | 84 ++++++++++++++---------------------
5 files changed, 79 insertions(+), 53 deletions(-)
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index c1459b81f..dcf6bb102 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -869,7 +869,7 @@ async def new(
echo(f'Using template `{template}`\n')
config_dict = {}
else:
- template, config_dict = template_from_terminal()
+ template, config_dict = template_from_terminal(answers['package'])
except Escape:
return
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 0020e5b8c..fd9b159b7 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -30,8 +30,10 @@
from typing import Annotated
from typing import Any
from typing import Literal
+from typing import Self
from typing import TypeVar
from typing import cast
+from typing import get_args
from urllib.parse import quote_plus
import orjson
@@ -47,8 +49,10 @@
from dipdup import __spec_version__
from dipdup import env
from dipdup.config._mixin import CallbackMixin
+from dipdup.config._mixin import InteractiveMixin
from dipdup.config._mixin import NameMixin
from dipdup.config._mixin import ParentMixin
+from dipdup.config._mixin import TerminalOptions
from dipdup.exceptions import ConfigInitializationException
from dipdup.exceptions import ConfigurationError
from dipdup.exceptions import IndexAlreadyExistsError
@@ -561,7 +565,7 @@ class AdvancedConfig:
@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
-class DipDupConfig:
+class DipDupConfig(InteractiveMixin):
"""DipDup project configuration file
:param spec_version: Version of config specification, currently always `3.0`
@@ -617,6 +621,29 @@ def schema_name(self) -> str:
def package_path(self) -> Path:
return env.get_package_path(self.package)
+ @classmethod
+ def from_terminal(cls, opts: TerminalOptions) -> Self:
+ from dipdup.project import fill_config_from_input
+
+ config_dict = {}
+
+ # NOTE: Substrate or multichain
+ if opts.namespace in {'substrate', None}:
+ fill_config_from_input(config_dict, 'runtimes', get_args(RuntimeConfigU), opts.namespace)
+
+ fill_config_from_input(config_dict, 'datasources', get_args(DatasourceConfigU), opts.namespace)
+ fill_config_from_input(config_dict, 'indexes', get_args(IndexConfigU), opts.namespace)
+ fill_config_from_input(config_dict, 'hooks', (HookConfig,), None)
+ fill_config_from_input(config_dict, 'jobs', (JobConfig,), None)
+
+ config_dict = {
+ 'package': opts.package,
+ 'spec_version': '3.0',
+ **config_dict,
+ }
+
+ return cls(**config_dict)
+
@classmethod
def load(
cls,
diff --git a/src/dipdup/config/_mixin.py b/src/dipdup/config/_mixin.py
index 270e88ca4..98890924d 100644
--- a/src/dipdup/config/_mixin.py
+++ b/src/dipdup/config/_mixin.py
@@ -7,6 +7,7 @@
from typing import TYPE_CHECKING
from typing import Any
from typing import Generic
+from typing import Self
from typing import TypeVar
from typing import cast
@@ -121,3 +122,17 @@ def subgroup_index(self) -> int:
@subgroup_index.setter
def subgroup_index(self, value: int) -> None:
self._subgroup_index = value
+
+
+@dataclass
+class TerminalOptions:
+ package: str
+ namespace: str | None = None
+
+
+@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
+class InteractiveMixin:
+
+ @classmethod
+ @abstractmethod
+ def from_terminal(cls, opts: TerminalOptions) -> Self: ...
diff --git a/src/dipdup/config/coinbase.py b/src/dipdup/config/coinbase.py
index c6b8a8f88..a1dc34918 100644
--- a/src/dipdup/config/coinbase.py
+++ b/src/dipdup/config/coinbase.py
@@ -29,5 +29,5 @@ class CoinbaseDatasourceConfig(DatasourceConfig):
http: HttpConfig | None = None
@property
- def url(self) -> str:
+ def url(self) -> str: # type: ignore[override]
return 'https://api.pro.coinbase.com'
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index adc891660..5355393f4 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -9,7 +9,6 @@
from itertools import chain
from pathlib import Path
from typing import Any
-from typing import get_args
from pydantic import ConfigDict
from pydantic import TypeAdapter
@@ -20,12 +19,9 @@
from dipdup import __version__
from dipdup.cli import big_yellow_echo
from dipdup.cli import echo
-from dipdup.config import DatasourceConfigU
-from dipdup.config import HookConfig
-from dipdup.config import IndexConfigU
-from dipdup.config import JobConfig
-from dipdup.config import RuntimeConfigU
+from dipdup.config import DipDupConfig
from dipdup.config import ToStr
+from dipdup.config._mixin import TerminalOptions
from dipdup.env import get_package_path
from dipdup.env import get_pyproject_name
from dipdup.utils import load_template
@@ -138,25 +134,7 @@ def get_replay_path(name: str) -> Path:
return Path(__file__).parent / 'projects' / name / 'replay.yaml'
-def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
- _, mode = prompt_anyof(
- question='How would you like to set up your new DipDup project?',
- options=(
- 'Interactively',
- 'From template',
- 'Blank',
- ),
- comments=(
- 'Guided setup with prompts',
- 'Use one of demo projects',
- 'Begin with an empty project',
- ),
- default=0,
- )
-
- if mode == 'Blank':
- return ('demo_blank', None)
-
+def namespace_from_terminal() -> str | None:
res = prompt_anyof(
question='What blockchain are you going to index?',
options=(
@@ -175,12 +153,38 @@ def template_from_terminal() -> tuple[str | None, dict[str, Any] | None]:
),
default=0,
)
- namespace = res[1].lower() if res[1] != '[multiple]' else None
+ return res[1].lower() if res[1] != '[multiple]' else None
+
+
+def template_from_terminal(package: str) -> tuple[str | None, dict[str, Any] | None]:
+ _, mode = prompt_anyof(
+ question='How would you like to set up your new DipDup project?',
+ options=(
+ 'Interactively',
+ 'From template',
+ 'Blank',
+ ),
+ comments=(
+ 'Guided setup with prompts',
+ 'Use one of demo projects',
+ 'Begin with an empty project',
+ ),
+ default=0,
+ )
+
+ if mode == 'Blank':
+ return ('demo_blank', None)
+
+ namespace = namespace_from_terminal()
if mode == 'Interactively':
replay_path = get_replay_path('demo_blank')
- config_dict = config_from_terminal(namespace)
- return ('demo_blank', config_dict)
+ opts = TerminalOptions(
+ package=package,
+ namespace=namespace,
+ )
+ config = DipDupConfig.from_terminal(opts)
+ return ('demo_blank', config._json)
if mode == 'From template':
options, comments = [], []
@@ -431,8 +435,6 @@ def fill_config_from_input(
section_dict = config.get(section, {})
another = False
- print(f'Configuring `{section}` section \n')
-
while True:
entity_singular = SINGULAR_FORMS[section]
@@ -463,7 +465,6 @@ def fill_config_from_input(
# NOTE: Prepare a filtered (or not) list of entity types
# Ask for the type of the entity
- print(f'Available {entity_singular} kinds:')
matched = {}
for entity_type in section_kinds:
try:
@@ -481,8 +482,6 @@ def fill_config_from_input(
if len(matched) != 1:
kinds = sorted(matched.keys())
comments = tuple('' for _ in kinds)
- print(f'Available {entity_singular} kinds: {kinds} {comments}')
-
_, kind = prompt_anyof(
f'Choose {entity_singular} kind: ',
kinds,
@@ -514,7 +513,7 @@ def fill_config_from_input(
# field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
field_value = survey.routines.input(
f'Enter value for `{field_name}` [{field.type}]: ',
- value=default or '',
+ value=str(default) if default is not None else '',
)
if field_value == '':
field_value = default
@@ -529,7 +528,7 @@ def fill_config_from_input(
# Validate and add the entity
try:
entity_instance = entity_model(**entity_data)
- section_dict[name] = entity_instance.__dict__
+ section_dict[name] = {k: v for k, v in entity_instance.__dict__.items() if not k.startswith('_')}
another = True
except Exception as e:
print(f'Error: {e}. Please try again.')
@@ -559,18 +558,3 @@ def prompt_anyof(
index=default,
)
return index, options[index]
-
-
-def config_from_terminal(namespace: str | None) -> dict[str, Any]:
- config_dict = {}
-
- # NOTE: Substrate or multichain
- if namespace in {'substrate', None}:
- fill_config_from_input(config_dict, 'runtimes', get_args(RuntimeConfigU), namespace)
-
- fill_config_from_input(config_dict, 'datasources', get_args(DatasourceConfigU), namespace)
- fill_config_from_input(config_dict, 'indexes', get_args(IndexConfigU), namespace)
- fill_config_from_input(config_dict, 'hooks', (HookConfig,), None)
- fill_config_from_input(config_dict, 'jobs', (JobConfig,), None)
-
- return config_dict
From bf647d583ccc53b444033c6a186921f3285d5b7a Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 15 Feb 2025 17:12:20 -0300
Subject: [PATCH 05/30] working defaults and bools
---
src/dipdup/config/__init__.py | 4 +++
src/dipdup/project.py | 56 +++++++++++++++++++++--------------
2 files changed, 38 insertions(+), 22 deletions(-)
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index fd9b159b7..891878632 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -295,6 +295,10 @@ class DatasourceConfig(ABC, NameMixin):
ws_url: WsUrl | None = None
http: HttpConfig | None = None
+ # @classmethod
+ # def from_terminal(cls, opts):
+ # return super().from_terminal(opts)
+
@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
class HandlerConfig(CallbackMixin, ParentMixin['IndexConfig']):
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index 5355393f4..3eccdc216 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -424,6 +424,20 @@ def _render(answers: Answers, template_path: Path, output_path: Path, force: boo
write(output_path, content, overwrite=force)
+def prompt_bool(
+ question: str,
+ default: bool = False,
+) -> bool:
+ choices = ('yes', 'no') if default else ('no', 'yes')
+ _, value = prompt_anyof(
+ question,
+ choices,
+ ('', ''),
+ default=0,
+ )
+ return value == 'yes'
+
+
def fill_config_from_input(
config: dict[str, Any],
section: str,
@@ -437,24 +451,12 @@ def fill_config_from_input(
while True:
entity_singular = SINGULAR_FORMS[section]
+ another_str = 'another' if another else 'the first'
- if another:
- another_str = 'another'
- choices = ('no', 'yes')
- else:
- another_str = 'the first'
- choices = (
- 'yes',
- 'no',
- )
-
- _, add_entity = prompt_anyof(
+ if not prompt_bool(
f'Do you want to add {another_str} {entity_singular}?',
- choices,
- ('', ''),
- default=0,
- )
- if add_entity != 'yes':
+ default=not another,
+ ):
break
# NOTE: All sections are mappings alias to dict
@@ -496,6 +498,8 @@ def fill_config_from_input(
# Gather input for the fields of the chosen type
entity_data = {}
for field_name, field in entity_model.__dataclass_fields__.items():
+ # print(field)
+
if field_name in {'kind', 'http'}:
continue
if field_name.startswith('_'):
@@ -505,26 +509,34 @@ def fill_config_from_input(
continue
default = field.default
+
if default == dataclasses.MISSING:
default = None
elif isinstance(default, FieldInfo):
default = default.default_factory() if default.default_factory else default.default
- # field_value = input(f'Enter value for `{field_name}` [{field.type}] ({default}): ').strip()
field_value = survey.routines.input(
f'Enter value for `{field_name}` [{field.type}]: ',
value=str(default) if default is not None else '',
)
- if field_value == '':
- field_value = default
+
+ if field.default_factory == dataclasses.MISSING:
+ default = None
# NOTE: Basic transformations: string lists
- if isinstance(field_value, str):
- if ',' in field_value:
+ if 'tuple' in field.type or 'list' in field.type:
+ if not field_value:
+ field_value = default
+ elif ',' in field_value:
field_value = field_value.split(',')
+ else:
+ field_value = [field_value]
entity_data[field_name] = field_value or default
+ # print(default, 'default')
+ # print(entity_data[field_name], 'field_value')
+
# Validate and add the entity
try:
entity_instance = entity_model(**entity_data)
@@ -533,7 +545,7 @@ def fill_config_from_input(
except Exception as e:
print(f'Error: {e}. Please try again.')
- print(section_dict)
+ # print(section_dict)
config[section] = section_dict
From dd41b86899ccea11f2d07f8378cb4582f3910edd Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 15 Feb 2025 18:33:21 -0300
Subject: [PATCH 06/30] ref
---
src/dipdup/project.py | 131 ++++++++++++++++++++++--------------------
1 file changed, 70 insertions(+), 61 deletions(-)
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index 3eccdc216..d01c10b49 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -281,29 +281,29 @@ def answers_from_terminal() -> Answers:
fg='yellow',
)
- big_yellow_echo('Miscellaneous tunables; leave default values if unsure')
-
- _, answers['package_manager'] = prompt_anyof(
- question='Choose package manager',
- options=(
- 'uv',
- 'poetry',
- 'pdm',
- 'none',
- ),
- comments=(
- 'uv (recommended)',
- 'Poetry',
- 'PDM',
- '[none]',
- ),
- default=0,
- )
-
- answers['line_length'] = survey.routines.input(
- 'Enter maximum line length for linters: ',
- value=answers['line_length'],
- )
+ # big_yellow_echo('Miscellaneous tunables; leave default values if unsure')
+
+ # _, answers['package_manager'] = prompt_anyof(
+ # question='Choose package manager',
+ # options=(
+ # 'uv',
+ # 'poetry',
+ # 'pdm',
+ # 'none',
+ # ),
+ # comments=(
+ # 'uv (recommended)',
+ # 'Poetry',
+ # 'PDM',
+ # '[none]',
+ # ),
+ # default=0,
+ # )
+
+ # answers['line_length'] = survey.routines.input(
+ # 'Enter maximum line length for linters: ',
+ # value=answers['line_length'],
+ # )
return answers
@@ -438,10 +438,45 @@ def prompt_bool(
return value == 'yes'
+def prompt_kind(
+ entity: str,
+ types: tuple[type, ...],
+ filter: str | None,
+) -> type:
+
+ matched = {}
+ for entity_type in types:
+ try:
+ kind = entity_type.__dataclass_fields__['kind'].default
+ except KeyError:
+ kind = entity_type.__name__
+
+ assert kind
+
+ if filter and '.' in kind and not kind.startswith(filter):
+ continue
+
+ matched[kind] = entity_type
+
+ if len(matched) == 1:
+ return next(iter(matched))
+
+ kinds = sorted(matched.keys())
+ comments = tuple('' for _ in kinds)
+ _, kind = prompt_anyof(
+ f'Choose {entity} kind: ',
+ kinds,
+ comments,
+ default=0,
+ )
+ return matched[kind]
+
+
+
def fill_config_from_input(
config: dict[str, Any],
section: str,
- section_kinds: tuple[type],
+ types: tuple[type, ...],
filter: str | None,
) -> None:
import survey
@@ -450,54 +485,28 @@ def fill_config_from_input(
another = False
while True:
- entity_singular = SINGULAR_FORMS[section]
- another_str = 'another' if another else 'the first'
+ section_singular = SINGULAR_FORMS[section]
if not prompt_bool(
- f'Do you want to add {another_str} {entity_singular}?',
+ f'Do you want to add {'another' if another else 'the first'} {section_singular}?',
default=not another,
):
break
# NOTE: All sections are mappings alias to dict
name = survey.routines.input(
- f'Enter {entity_singular} name: ',
+ f'Enter {section_singular} name: ',
)
- # NOTE: Prepare a filtered (or not) list of entity types
-
- # Ask for the type of the entity
- matched = {}
- for entity_type in section_kinds:
- try:
- kind = entity_type.__dataclass_fields__['kind'].default
- except KeyError:
- kind = entity_type.__name__
-
- assert kind
-
- if filter and '.' in kind and not kind.startswith(filter):
- continue
-
- matched[kind] = entity_type
-
- if len(matched) != 1:
- kinds = sorted(matched.keys())
- comments = tuple('' for _ in kinds)
- _, kind = prompt_anyof(
- f'Choose {entity_singular} kind: ',
- kinds,
- comments,
- default=0,
- )
- else:
- kind = next(iter(matched))
-
- entity_model = matched[kind]
+ type_ = prompt_kind(
+ section_singular,
+ types,
+ filter,
+ )
# Gather input for the fields of the chosen type
entity_data = {}
- for field_name, field in entity_model.__dataclass_fields__.items():
+ for field_name, field in type_.__dataclass_fields__.items():
# print(field)
if field_name in {'kind', 'http'}:
@@ -539,8 +548,8 @@ def fill_config_from_input(
# Validate and add the entity
try:
- entity_instance = entity_model(**entity_data)
- section_dict[name] = {k: v for k, v in entity_instance.__dict__.items() if not k.startswith('_')}
+ obj = type_(**entity_data)
+ section_dict[name] = {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
another = True
except Exception as e:
print(f'Error: {e}. Please try again.')
From b49200ad9168d4be58f65bf0ffc2d0315039fe88 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 15 Feb 2025 19:00:13 -0300
Subject: [PATCH 07/30] from_terminal with failover
---
src/dipdup/config/__init__.py | 55 +++++++++++++--
src/dipdup/project.py | 122 +++++++++++++---------------------
2 files changed, 93 insertions(+), 84 deletions(-)
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 891878632..74c32262f 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -627,18 +627,59 @@ def package_path(self) -> Path:
@classmethod
def from_terminal(cls, opts: TerminalOptions) -> Self:
- from dipdup.project import fill_config_from_input
+ import survey
- config_dict = {}
+ from dipdup.project import SINGULAR_FORMS
+ from dipdup.project import fill_type_from_input
+ from dipdup.project import prompt_bool
+ from dipdup.project import prompt_kind
+ config_dict = defaultdict(dict)
+
+ sections = {
+ 'datasources': get_args(DatasourceConfigU),
+ 'runtimes': get_args(RuntimeConfigU),
+ 'contracts': get_args(ContractConfigU),
+ # NOTE: Skip the `template` kind
+ 'indexes': get_args(ResolvedIndexConfigU),
+ }
# NOTE: Substrate or multichain
if opts.namespace in {'substrate', None}:
- fill_config_from_input(config_dict, 'runtimes', get_args(RuntimeConfigU), opts.namespace)
+ sections['runtimes'] = get_args(RuntimeConfigU)
+
+ for section, types in sections.items():
+ another = False
+
+ while True:
+ section_singular = SINGULAR_FORMS[section]
+
+ if not prompt_bool(
+ f'Do you want to add {'another' if another else 'the first'} {section_singular}?',
+ default=not another,
+ ):
+ break
+
+ # NOTE: All sections are mappings alias to dict
+ name = survey.routines.input(
+ f'Enter {section_singular} name: ',
+ )
+
+ type_ = prompt_kind(
+ section_singular,
+ types,
+ opts.namespace,
+ )
+
+ if issubclass(type_, InteractiveMixin):
+
+ res = type_.from_terminal(opts)
+ else:
+ _logger.debug('Not an `InteractiveMixin`; falling back to field inspection', type_.__name__)
+ res = fill_type_from_input(type_)
- fill_config_from_input(config_dict, 'datasources', get_args(DatasourceConfigU), opts.namespace)
- fill_config_from_input(config_dict, 'indexes', get_args(IndexConfigU), opts.namespace)
- fill_config_from_input(config_dict, 'hooks', (HookConfig,), None)
- fill_config_from_input(config_dict, 'jobs', (JobConfig,), None)
+ if res is not None:
+ config_dict[section][name] = res
+ another = True
config_dict = {
'package': opts.package,
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index d01c10b49..592156bcc 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -471,92 +471,60 @@ def prompt_kind(
)
return matched[kind]
-
-def fill_config_from_input(
- config: dict[str, Any],
- section: str,
- types: tuple[type, ...],
- filter: str | None,
-) -> None:
+def fill_type_from_input(
+ type_: type,
+) -> Any:
import survey
- section_dict = config.get(section, {})
- another = False
+ # Gather input for the fields of the chosen type
+ entity_data = {}
+ for field_name, field in type_.__dataclass_fields__.items():
+ # print(field)
- while True:
- section_singular = SINGULAR_FORMS[section]
+ if field_name in {'kind', 'http'}:
+ continue
+ if field_name.startswith('_'):
+ continue
+ if field_name == 'handlers':
+ entity_data[field_name] = ()
+ continue
- if not prompt_bool(
- f'Do you want to add {'another' if another else 'the first'} {section_singular}?',
- default=not another,
- ):
- break
+ default = field.default
- # NOTE: All sections are mappings alias to dict
- name = survey.routines.input(
- f'Enter {section_singular} name: ',
- )
+ if default == dataclasses.MISSING:
+ default = None
+ elif isinstance(default, FieldInfo):
+ default = default.default_factory() if default.default_factory else default.default
- type_ = prompt_kind(
- section_singular,
- types,
- filter,
+ field_value = survey.routines.input(
+ f'Enter value for `{field_name}` [{field.type}]: ',
+ value=str(default) if default is not None else '',
)
- # Gather input for the fields of the chosen type
- entity_data = {}
- for field_name, field in type_.__dataclass_fields__.items():
- # print(field)
-
- if field_name in {'kind', 'http'}:
- continue
- if field_name.startswith('_'):
- continue
- if field_name == 'handlers':
- entity_data[field_name] = ()
- continue
-
- default = field.default
-
- if default == dataclasses.MISSING:
- default = None
- elif isinstance(default, FieldInfo):
- default = default.default_factory() if default.default_factory else default.default
-
- field_value = survey.routines.input(
- f'Enter value for `{field_name}` [{field.type}]: ',
- value=str(default) if default is not None else '',
- )
-
- if field.default_factory == dataclasses.MISSING:
- default = None
-
- # NOTE: Basic transformations: string lists
- if 'tuple' in field.type or 'list' in field.type:
- if not field_value:
- field_value = default
- elif ',' in field_value:
- field_value = field_value.split(',')
- else:
- field_value = [field_value]
-
- entity_data[field_name] = field_value or default
-
- # print(default, 'default')
- # print(entity_data[field_name], 'field_value')
-
- # Validate and add the entity
- try:
- obj = type_(**entity_data)
- section_dict[name] = {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
- another = True
- except Exception as e:
- print(f'Error: {e}. Please try again.')
-
- # print(section_dict)
-
- config[section] = section_dict
+ if field.default_factory == dataclasses.MISSING:
+ default = None
+
+ # NOTE: Basic transformations: string lists
+ if 'tuple' in field.type or 'list' in field.type:
+ if not field_value:
+ field_value = default
+ elif ',' in field_value:
+ field_value = field_value.split(',')
+ else:
+ field_value = [field_value]
+
+ entity_data[field_name] = field_value or default
+
+ # print(default, 'default')
+ # print(entity_data[field_name], 'field_value')
+
+ # Validate and add the entity
+ try:
+ obj = type_(**entity_data)
+ return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
+ except Exception as e:
+ print(f'Error: {e}. Please try again.')
def prompt_anyof(
From 2eb80f534cc55994f7301b58d926ab6e9526f7b2 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sun, 16 Feb 2025 19:38:30 -0300
Subject: [PATCH 08/30] misc fixes
---
docs/9.release-notes/1.v8.2.md | 1 -
docs/9.release-notes/_8.0_changelog.md | 2 +
docs/9.release-notes/_8.2_changelog.md | 2 +-
schemas/dipdup-3.0.json | 99 ++++++++++++--------------
src/dipdup/cli.py | 9 +--
src/dipdup/config/__init__.py | 25 +++++--
src/dipdup/config/evm_etherscan.py | 12 ++--
src/dipdup/config/http.py | 3 +-
src/dipdup/config/ipfs.py | 3 +-
src/dipdup/config/substrate_subscan.py | 3 +-
src/dipdup/config/tzip_metadata.py | 3 +-
src/dipdup/project.py | 27 +++----
12 files changed, 101 insertions(+), 88 deletions(-)
diff --git a/docs/9.release-notes/1.v8.2.md b/docs/9.release-notes/1.v8.2.md
index 8b8bee710..081b6a02e 100644
--- a/docs/9.release-notes/1.v8.2.md
+++ b/docs/9.release-notes/1.v8.2.md
@@ -44,6 +44,5 @@ DipDup 7.5, our previous major release, has reached end-of-life. We recommend up
Going forward, we'll focus on supporting only the latest major version to reduce maintenance overhead. Any breaking changes will be introduced gradually and can be enabled using the `DIPDUP_NEXT` environment variable.
-
{{ #include 9.release-notes/_8.2_changelog.md }}
{{ #include 9.release-notes/_footer.md }}
diff --git a/docs/9.release-notes/_8.0_changelog.md b/docs/9.release-notes/_8.0_changelog.md
index e0555feed..afde020c6 100644
--- a/docs/9.release-notes/_8.0_changelog.md
+++ b/docs/9.release-notes/_8.0_changelog.md
@@ -9,6 +9,7 @@
- cli: Added `package verify` command to check the package consistency.
- cli: Added full project migration support for 3.0 spec.
- cli: Added spec_version 3.0 support to `migrate` command.
+- cli: Rewritten interactive mode for `new` command.
- config: Publish JSON schemas for config validation and autocompletion.
- database: Added `dipdup_status` view to the schema.
- env: Added `DIPDUP_JSON_LOG` environment variable to enable JSON logging.
@@ -30,6 +31,7 @@
- cli: Fixed progress estimation when there are indexes with `last_level` option set.
- cli: Import some dependencies on demand to reduce memory footprint.
- cli: Improved logging of indexer status.
+- coinbase: Fixed crash when using coinbase datasource.
- config: Allow `sentry.dsn` to be empty string.
- config: Fixed (de)serialization of hex strings in config.
- config: Fixed setting logging levels according to the config.
diff --git a/docs/9.release-notes/_8.2_changelog.md b/docs/9.release-notes/_8.2_changelog.md
index fa1ece0a8..66e0d7082 100644
--- a/docs/9.release-notes/_8.2_changelog.md
+++ b/docs/9.release-notes/_8.2_changelog.md
@@ -15,8 +15,8 @@
- cli: Fixed help message on `CallbackError` reporting `batch` handler instead of actual one.
- database: Don't process internal models twice if imported from the project.
- evm.subsquid: Fixed event/transaction model deserialization.
-- starknet: Process all data types correctly.
- starknet.node: Fetch missing block timestamp and txn id when synching with node.
+- starknet: Process all data types correctly.
- substrate.subsquid: Fixed parsing for `__kind` junctions with multiple keys.
- substrate.subsquid: Fixed parsing nested structures in response.
diff --git a/schemas/dipdup-3.0.json b/schemas/dipdup-3.0.json
index 04dc08524..b2df1ad24 100644
--- a/schemas/dipdup-3.0.json
+++ b/schemas/dipdup-3.0.json
@@ -134,6 +134,7 @@
},
"kind": {
"const": "coinbase",
+ "default": "coinbase",
"description": "always 'coinbase'",
"title": "kind",
"type": "string"
@@ -165,9 +166,6 @@
"title": "secret_key"
}
},
- "required": [
- "kind"
- ],
"title": "CoinbaseDatasourceConfig",
"type": "object"
},
@@ -212,6 +210,7 @@
},
"kind": {
"const": "evm",
+ "default": "evm",
"description": "Always `evm`",
"title": "kind",
"type": "string"
@@ -230,9 +229,6 @@
"title": "typename"
}
},
- "required": [
- "kind"
- ],
"title": "EvmContractConfig",
"type": "object"
},
@@ -277,17 +273,17 @@
"type": "string"
}
],
+ "default": "evm.etherscan",
"description": "always 'evm.etherscan'",
"title": "kind"
},
"url": {
+ "$ref": "#/$defs/Url",
"description": "API URL",
- "title": "url",
- "type": "string"
+ "title": "url"
}
},
"required": [
- "kind",
"url"
],
"title": "EvmEtherscanDatasourceConfig",
@@ -369,6 +365,7 @@
},
"kind": {
"const": "evm.events",
+ "default": "evm.events",
"description": "Always 'evm.events'",
"title": "kind",
"type": "string"
@@ -381,7 +378,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -407,6 +403,7 @@
},
"kind": {
"const": "evm.node",
+ "default": "evm.node",
"description": "Always 'evm.node'",
"title": "kind",
"type": "string"
@@ -437,7 +434,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "EvmNodeDatasourceConfig",
@@ -462,6 +458,7 @@
},
"kind": {
"const": "evm.subsquid",
+ "default": "evm.subsquid",
"description": "always 'evm.subsquid'",
"title": "kind",
"type": "string"
@@ -473,7 +470,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "EvmSubsquidDatasourceConfig",
@@ -594,6 +590,7 @@
},
"kind": {
"const": "evm.transactions",
+ "default": "evm.transactions",
"description": "always 'evm.transactions'",
"title": "kind",
"type": "string"
@@ -606,7 +603,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -957,18 +953,18 @@
},
"kind": {
"const": "http",
+ "default": "http",
"description": "always 'http'",
"title": "kind",
"type": "string"
},
"url": {
+ "$ref": "#/$defs/Url",
"description": "URL to fetch data from",
- "title": "url",
- "type": "string"
+ "title": "url"
}
},
"required": [
- "kind",
"url"
],
"title": "HttpDatasourceConfig",
@@ -984,6 +980,13 @@
"title": "first_level",
"type": "integer"
},
+ "kind": {
+ "const": "template",
+ "default": "template",
+ "description": "always 'template'",
+ "title": "kind",
+ "type": "string"
+ },
"last_level": {
"default": 0,
"description": "Level to stop indexing at",
@@ -1027,20 +1030,18 @@
},
"kind": {
"const": "ipfs",
+ "default": "ipfs",
"description": "always 'ipfs'",
"title": "kind",
"type": "string"
},
"url": {
+ "$ref": "#/$defs/Url",
"default": "https://ipfs.io/ipfs",
"description": "IPFS node URL, e.g. https://ipfs.io/ipfs/",
- "title": "url",
- "type": "string"
+ "title": "url"
}
},
- "required": [
- "kind"
- ],
"title": "IpfsDatasourceConfig",
"type": "object"
},
@@ -1136,6 +1137,7 @@
},
"kind": {
"const": "postgres",
+ "default": "postgres",
"description": "always 'postgres'",
"title": "kind",
"type": "string"
@@ -1166,7 +1168,6 @@
}
},
"required": [
- "kind",
"host"
],
"title": "PostgresDatabaseConfig",
@@ -1324,6 +1325,7 @@
},
"kind": {
"const": "sqlite",
+ "default": "sqlite",
"description": "always 'sqlite'",
"title": "kind",
"type": "string"
@@ -1335,9 +1337,6 @@
"type": "string"
}
},
- "required": [
- "kind"
- ],
"title": "SqliteDatabaseConfig",
"type": "object"
},
@@ -1382,6 +1381,7 @@
},
"kind": {
"const": "starknet",
+ "default": "starknet",
"description": "Always `starknet`",
"title": "kind",
"type": "string"
@@ -1400,9 +1400,6 @@
"title": "typename"
}
},
- "required": [
- "kind"
- ],
"title": "StarknetContractConfig",
"type": "object"
},
@@ -1479,6 +1476,7 @@
},
"kind": {
"const": "starknet.events",
+ "default": "starknet.events",
"description": "Always 'starknet.events'",
"title": "kind",
"type": "string"
@@ -1491,7 +1489,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -1517,6 +1514,7 @@
},
"kind": {
"const": "starknet.node",
+ "default": "starknet.node",
"description": "Always 'starknet.node'",
"title": "kind",
"type": "string"
@@ -1547,7 +1545,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "StarknetNodeDatasourceConfig",
@@ -1572,6 +1569,7 @@
},
"kind": {
"const": "starknet.subsquid",
+ "default": "starknet.subsquid",
"description": "always 'starknet.subsquid'",
"title": "kind",
"type": "string"
@@ -1583,7 +1581,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "StarknetSubsquidDatasourceConfig",
@@ -1652,6 +1649,7 @@
},
"kind": {
"const": "substrate.events",
+ "default": "substrate.events",
"description": "Always 'substrate.events'",
"title": "kind",
"type": "string"
@@ -1676,7 +1674,6 @@
}
},
"required": [
- "kind",
"datasources",
"runtime",
"handlers"
@@ -1703,6 +1700,7 @@
},
"kind": {
"const": "substrate.node",
+ "default": "substrate.node",
"description": "Always 'substrate.node'",
"title": "kind",
"type": "string"
@@ -1727,7 +1725,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "SubstrateNodeDatasourceConfig",
@@ -1793,18 +1790,18 @@
},
"kind": {
"const": "substrate.subscan",
+ "default": "substrate.subscan",
"description": "always 'substrate.subscan'",
"title": "kind",
"type": "string"
},
"url": {
+ "$ref": "#/$defs/Url",
"description": "API URL",
- "title": "url",
- "type": "string"
+ "title": "url"
}
},
"required": [
- "kind",
"url"
],
"title": "SubstrateSubscanDatasourceConfig",
@@ -1829,6 +1826,7 @@
},
"kind": {
"const": "substrate.subsquid",
+ "default": "substrate.subsquid",
"description": "always 'substrate.subsquid'",
"title": "kind",
"type": "string"
@@ -1840,7 +1838,6 @@
}
},
"required": [
- "kind",
"url"
],
"title": "SubstrateSubsquidDatasourceConfig",
@@ -1919,6 +1916,7 @@
},
"kind": {
"const": "tezos.big_maps",
+ "default": "tezos.big_maps",
"description": "always 'tezos.big_maps'",
"title": "kind",
"type": "string"
@@ -1937,7 +1935,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -1979,6 +1976,7 @@
},
"kind": {
"const": "tezos",
+ "default": "tezos",
"description": "Always `tezos`",
"title": "kind",
"type": "string"
@@ -1997,9 +1995,6 @@
"title": "typename"
}
},
- "required": [
- "kind"
- ],
"title": "TezosContractConfig",
"type": "object"
},
@@ -2080,6 +2075,7 @@
},
"kind": {
"const": "tezos.events",
+ "default": "tezos.events",
"description": "always 'tezos.events'",
"title": "kind",
"type": "string"
@@ -2092,7 +2088,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -2154,13 +2149,13 @@
},
"kind": {
"const": "tezos.head",
+ "default": "tezos.head",
"description": "always 'tezos.head'",
"title": "kind",
"type": "string"
}
},
"required": [
- "kind",
"datasources",
"callback"
],
@@ -2550,6 +2545,7 @@
},
"kind": {
"const": "tezos.operations",
+ "default": "tezos.operations",
"description": "always 'tezos.operations'",
"title": "kind",
"type": "string"
@@ -2573,7 +2569,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -2612,6 +2607,7 @@
},
"kind": {
"const": "tezos.operations_unfiltered",
+ "default": "tezos.operations_unfiltered",
"description": "always 'tezos.operations_unfiltered'",
"title": "kind",
"type": "string"
@@ -2635,7 +2631,6 @@
}
},
"required": [
- "kind",
"datasources",
"callback"
],
@@ -2722,6 +2717,7 @@
},
"kind": {
"const": "tezos.token_balances",
+ "default": "tezos.token_balances",
"description": "always 'tezos.token_balances'",
"title": "kind",
"type": "string"
@@ -2734,7 +2730,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -2853,6 +2848,7 @@
},
"kind": {
"const": "tezos.token_transfers",
+ "default": "tezos.token_transfers",
"description": "always 'tezos.token_transfers'",
"title": "kind",
"type": "string"
@@ -2865,7 +2861,6 @@
}
},
"required": [
- "kind",
"datasources",
"handlers"
],
@@ -2897,6 +2892,7 @@
},
"kind": {
"const": "tezos.tzkt",
+ "default": "tezos.tzkt",
"description": "always 'tezos.tzkt'",
"title": "kind",
"type": "string"
@@ -2920,9 +2916,6 @@
"title": "url"
}
},
- "required": [
- "kind"
- ],
"title": "TezosTzktDatasourceConfig",
"type": "object"
},
@@ -2955,6 +2948,7 @@
},
"kind": {
"const": "tzip_metadata",
+ "default": "tzip_metadata",
"description": "always 'tzip_metadata'",
"title": "kind",
"type": "string"
@@ -2965,14 +2959,13 @@
"title": "network"
},
"url": {
+ "$ref": "#/$defs/Url",
"default": "https://metadata.dipdup.net",
"description": "GraphQL API URL, e.g. https://metadata.dipdup.net",
- "title": "url",
- "type": "string"
+ "title": "url"
}
},
"required": [
- "kind",
"network"
],
"title": "TzipMetadataDatasourceConfig",
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index dcf6bb102..5d054fee3 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -867,7 +867,7 @@ async def new(
if template:
echo(f'Using template `{template}`\n')
- config_dict = {}
+ config_dict: dict[str, Any] | None = {}
else:
template, config_dict = template_from_terminal(answers['package'])
@@ -884,11 +884,8 @@ async def new(
'spec_version': '3.0',
**config_dict,
}
-
- config = DipDupYAMLConfig(**config_dict)
-
- path = env.get_package_path(config['package']) / ROOT_CONFIG
- path.write_text(config.dump())
+ path = env.get_package_path(config_dict['package']) / ROOT_CONFIG
+ path.write_text(DipDupYAMLConfig(**config_dict).dump())
_logger.info('Initializing project')
config = DipDupConfig.load([Path(answers['package'])])
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 74c32262f..32afa0d11 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -75,6 +75,8 @@
def _valid_url(v: str, ws: bool) -> str:
+ if not v:
+ raise ConfigurationError('URL is required')
if not ws and not v.startswith(('http://', 'https://')):
raise ConfigurationError(f'`{v}` is not a valid HTTP URL')
if ws and not v.startswith(('ws://', 'wss://')):
@@ -627,14 +629,14 @@ def package_path(self) -> Path:
@classmethod
def from_terminal(cls, opts: TerminalOptions) -> Self:
- import survey
+ import survey # type: ignore[import-untyped]
from dipdup.project import SINGULAR_FORMS
from dipdup.project import fill_type_from_input
from dipdup.project import prompt_bool
from dipdup.project import prompt_kind
- config_dict = defaultdict(dict)
+ config_dict: defaultdict[str, dict[str, Any]] = defaultdict(dict)
sections = {
'datasources': get_args(DatasourceConfigU),
@@ -660,9 +662,18 @@ def from_terminal(cls, opts: TerminalOptions) -> Self:
break
# NOTE: All sections are mappings alias to dict
- name = survey.routines.input(
- f'Enter {section_singular} name: ',
- )
+ name = None
+ while True:
+ name = survey.routines.input(
+ f'Enter {section_singular} name: ',
+ )
+ if not name:
+ print('Name is required')
+ continue
+ if name in config_dict[section]:
+ print(f'{section_singular.capitalize()} with name `{name}` already exists')
+ continue
+ break
type_ = prompt_kind(
section_singular,
@@ -681,13 +692,13 @@ def from_terminal(cls, opts: TerminalOptions) -> Self:
config_dict[section][name] = res
another = True
- config_dict = {
+ config_dict = { # type: ignore[assignment]
'package': opts.package,
'spec_version': '3.0',
**config_dict,
}
- return cls(**config_dict)
+ return cls(**config_dict) # type: ignore[arg-type]
@classmethod
def load(
diff --git a/src/dipdup/config/evm_etherscan.py b/src/dipdup/config/evm_etherscan.py
index 8e24df9d9..9d68377a2 100644
--- a/src/dipdup/config/evm_etherscan.py
+++ b/src/dipdup/config/evm_etherscan.py
@@ -6,8 +6,11 @@
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
+from dipdup import env
from dipdup.config import DatasourceConfig
from dipdup.config import HttpConfig
+from dipdup.config import Url
+from dipdup.exceptions import ConfigurationError
_logger = logging.getLogger(__name__)
@@ -24,13 +27,14 @@ class EvmEtherscanDatasourceConfig(DatasourceConfig):
# NOTE: Alias, remove in 9.0
kind: Literal['evm.etherscan'] | Literal['abi.etherscan'] = 'evm.etherscan'
- url: str
+ url: Url
api_key: str | None = None
http: HttpConfig | None = None
def __post_init__(self) -> None:
if self.kind == 'abi.etherscan':
- _logger.warning(
- '`abi.etherscan` datasource has been renamed to `evm.etherscan`. Please, update your config.'
- )
+ msg = '`abi.etherscan` datasource has been renamed to `evm.etherscan`. Please, update your config.'
+ if env.NEXT:
+ raise ConfigurationError(msg)
+ _logger.warning(msg)
diff --git a/src/dipdup/config/http.py b/src/dipdup/config/http.py
index b4341e90e..772e7c918 100644
--- a/src/dipdup/config/http.py
+++ b/src/dipdup/config/http.py
@@ -7,6 +7,7 @@
from dipdup.config import DatasourceConfig
from dipdup.config import HttpConfig
+from dipdup.config import Url
@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
@@ -19,5 +20,5 @@ class HttpDatasourceConfig(DatasourceConfig):
"""
kind: Literal['http'] = 'http'
- url: str
+ url: Url
http: HttpConfig | None = None
diff --git a/src/dipdup/config/ipfs.py b/src/dipdup/config/ipfs.py
index 90993b647..df1bc962d 100644
--- a/src/dipdup/config/ipfs.py
+++ b/src/dipdup/config/ipfs.py
@@ -7,6 +7,7 @@
from dipdup.config import DatasourceConfig
from dipdup.config import HttpConfig
+from dipdup.config import Url
DEFAULT_IPFS_URL = 'https://ipfs.io/ipfs'
@@ -21,5 +22,5 @@ class IpfsDatasourceConfig(DatasourceConfig):
"""
kind: Literal['ipfs'] = 'ipfs'
- url: str = DEFAULT_IPFS_URL
+ url: Url = DEFAULT_IPFS_URL
http: HttpConfig | None = None
diff --git a/src/dipdup/config/substrate_subscan.py b/src/dipdup/config/substrate_subscan.py
index 8167da3b4..296ab629e 100644
--- a/src/dipdup/config/substrate_subscan.py
+++ b/src/dipdup/config/substrate_subscan.py
@@ -7,6 +7,7 @@
from dipdup.config import DatasourceConfig
from dipdup.config import HttpConfig
+from dipdup.config import Url
@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
@@ -20,7 +21,7 @@ class SubstrateSubscanDatasourceConfig(DatasourceConfig):
"""
kind: Literal['substrate.subscan'] = 'substrate.subscan'
- url: str
+ url: Url
api_key: str | None = None
http: HttpConfig | None = None
diff --git a/src/dipdup/config/tzip_metadata.py b/src/dipdup/config/tzip_metadata.py
index b787fde43..52665c4db 100644
--- a/src/dipdup/config/tzip_metadata.py
+++ b/src/dipdup/config/tzip_metadata.py
@@ -7,6 +7,7 @@
from dipdup.config import DatasourceConfig
from dipdup.config import HttpConfig
+from dipdup.config import Url
from dipdup.models.tzip_metadata import TzipMetadataNetwork
DEFAULT_TZIP_METADATA_URL = 'https://metadata.dipdup.net'
@@ -24,5 +25,5 @@ class TzipMetadataDatasourceConfig(DatasourceConfig):
kind: Literal['tzip_metadata'] = 'tzip_metadata'
network: TzipMetadataNetwork
- url: str = DEFAULT_TZIP_METADATA_URL
+ url: Url = DEFAULT_TZIP_METADATA_URL
http: HttpConfig | None = None
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index 592156bcc..ae5618053 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -135,7 +135,7 @@ def get_replay_path(name: str) -> Path:
def namespace_from_terminal() -> str | None:
- res = prompt_anyof(
+ _, res = prompt_anyof(
question='What blockchain are you going to index?',
options=(
'EVM',
@@ -153,7 +153,10 @@ def namespace_from_terminal() -> str | None:
),
default=0,
)
- return res[1].lower() if res[1] != '[multiple]' else None
+ print(res)
+ if res == '[multiple]':
+ return None
+ return res.lower()
def template_from_terminal(package: str) -> tuple[str | None, dict[str, Any] | None]:
@@ -190,7 +193,7 @@ def template_from_terminal(package: str) -> tuple[str | None, dict[str, Any] | N
options, comments = [], []
templates = TEMPLATES[namespace] if namespace else chain(v for v in TEMPLATES.values())
for name in templates:
- replay_path = get_replay_path(name)
+ replay_path = get_replay_path(name) # type: ignore[arg-type]
_answers = answers_from_replay(replay_path)
options.append(_answers['template'])
comments.append(_answers['description'])
@@ -233,10 +236,10 @@ def answers_from_terminal() -> Answers:
)
answers['package'] = package
- answers['version'] = survey.routines.input(
- 'Enter project version: ',
- value=answers['version'],
- )
+ # answers['version'] = survey.routines.input(
+ # 'Enter project version: ',
+ # value=answers['version'],
+ # )
# NOTE: Used in pyproject.toml, README.md and some other places
answers['description'] = survey.routines.input(
@@ -447,7 +450,7 @@ def prompt_kind(
matched = {}
for entity_type in types:
try:
- kind = entity_type.__dataclass_fields__['kind'].default
+ kind = entity_type.__dataclass_fields__['kind'].default # type: ignore[attr-defined]
except KeyError:
kind = entity_type.__name__
@@ -459,9 +462,9 @@ def prompt_kind(
matched[kind] = entity_type
if len(matched) == 1:
- return next(iter(matched))
+ return next(iter(matched)) # type: ignore[no-any-return]
- kinds = sorted(matched.keys())
+ kinds = tuple(sorted(matched.keys()))
comments = tuple('' for _ in kinds)
_, kind = prompt_anyof(
f'Choose {entity} kind: ',
@@ -479,7 +482,7 @@ def fill_type_from_input(
# Gather input for the fields of the chosen type
entity_data = {}
- for field_name, field in type_.__dataclass_fields__.items():
+ for field_name, field in type_.__dataclass_fields__.items(): # type: ignore[attr-defined]
# print(field)
if field_name in {'kind', 'http'}:
@@ -495,7 +498,7 @@ def fill_type_from_input(
if default == dataclasses.MISSING:
default = None
elif isinstance(default, FieldInfo):
- default = default.default_factory() if default.default_factory else default.default
+ default = default.default_factory() if default.default_factory else default.default # type: ignore[call-arg]
field_value = survey.routines.input(
f'Enter value for `{field_name}` [{field.type}]: ',
From afab55fda92a73adfdccaeae8a31ca8e6f7ff2ab Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Mon, 17 Feb 2025 08:52:56 -0300
Subject: [PATCH 09/30] bump dmcg
---
requirements.txt | 30 ++++----
src/dipdup/config/__init__.py | 8 +-
uv.lock | 138 +++++++++++++++++-----------------
3 files changed, 89 insertions(+), 87 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 97acc296b..8dd56fada 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -172,9 +172,9 @@ cytoolz==1.0.1 ; implementation_name == 'cpython' \
--hash=sha256:c8231b9abbd8e368e036f4cc2e16902c9482d4cf9e02a6147ed0e9a3cd4a9ab0 \
--hash=sha256:fb988c333f05ee30ad4693fe4da55d95ec0bb05775d2b60191236493ea2e01f9 \
--hash=sha256:fcb8f7d0d65db1269022e7e0428471edee8c937bc288ebdcb72f13eaa67c2fe4
-datamodel-code-generator==0.27.2 \
- --hash=sha256:1a7655f5fd3a61329b57534904f5c40dd850850e420696fd946ec7a4f59c32b8 \
- --hash=sha256:efcbfbe6a1488d3411fc588b1ce1af5f854f5107810b1cc9026a6d6333a7c4d8
+datamodel-code-generator==0.28.1 \
+ --hash=sha256:1ff8a56f9550a82bcba3e1ad7ebdb89bc655eeabbc4bc6acfb05977cbdc6381c \
+ --hash=sha256:37ef5f3b488f7d7a3f0b5b3ba0f2bc1ae01bab4dc7e0f6b99ff6c40713a6beb3
dictdiffer==0.9.0 \
--hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \
--hash=sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595
@@ -190,9 +190,9 @@ eth-account==0.13.3 \
eth-hash==0.7.1 \
--hash=sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a \
--hash=sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5
-eth-keyfile==0.9.0 \
- --hash=sha256:45d3513b6433ad885370225ba0429ed26493ba23589c5b1ca5da024765020fef \
- --hash=sha256:8621c35e83cbc05909d2f23dbb8a87633918733caea553ae0e298f6a06291526
+eth-keyfile==0.9.1 \
+ --hash=sha256:9789c3b4fa0bb6e2616cdc2bdd71b8755b42947d78ef1e900a0149480fabb5c2 \
+ --hash=sha256:c7a8bc6af4527d1ab2eb1d1b949d59925252e17663eaf90087da121327b51df6
eth-keys==0.6.1 \
--hash=sha256:7deae4cd56e862e099ec58b78176232b931c4ea5ecded2f50c7b1ccbc10c24cf \
--hash=sha256:a43e263cbcabfd62fa769168efc6c27b1f5603040e4de22bb84d12567e4fd962
@@ -499,9 +499,9 @@ ruamel-yaml-clib==0.2.12 ; platform_python_implementation == 'CPython' \
scalecodec==1.2.11 \
--hash=sha256:99a2cdbfccdcaf22bd86b86da55a730a2855514ad2309faef4a4a93ac6cbeb8d \
--hash=sha256:d15c94965f617caa25096f83a45f5f73031d05e6ee08d6039969f0a64fc35de1
-sentry-sdk==2.20.0 \
- --hash=sha256:afa82713a92facf847df3c6f63cec71eb488d826a50965def3d7722aa6f0fdab \
- --hash=sha256:c359a1edf950eb5e80cffd7d9111f3dbeef57994cb4415df37d39fda2cf22364
+sentry-sdk==2.21.0 \
+ --hash=sha256:7623cfa9e2c8150948a81ca253b8e2bfe4ce0b96ab12f8cd78e3ac9c490fd92f \
+ --hash=sha256:a6d38e0fb35edda191acf80b188ec713c863aaa5ad8d5798decb8671d02077b6
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
@@ -530,9 +530,9 @@ toolz==1.0.0 ; implementation_name == 'cpython' or implementation_name == 'pypy'
tortoise-orm==0.24.0 \
--hash=sha256:ae0704a93ea27931724fc899e57268c8081afce3b32b110b00037ec206553e7d \
--hash=sha256:ee3b72b226767293b24c5c4906ae5f027d7cc84496cd503352c918564b4fd687
-typeguard==4.4.1 \
- --hash=sha256:0d22a89d00b453b47c49875f42b6601b961757541a2e1e0ef517b6e24213c21b \
- --hash=sha256:9324ec07a27ec67fc54a9c063020ca4c0ae6abad5e9f0f9804ca59aee68c6e21
+typeguard==4.4.2 \
+ --hash=sha256:77a78f11f09777aeae7fa08585f33b5f4ef0e7335af40005b0c422ed398ff48c \
+ --hash=sha256:a6f1065813e32ef365bc3b3f503af8a96f9dd4e0033a02c28c4a4983de8c6c49
types-requests==2.32.0.20241016 \
--hash=sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95 \
--hash=sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747
@@ -545,9 +545,9 @@ typing-inspect==0.9.0 \
tzdata==2025.1 ; sys_platform == 'win32' \
--hash=sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694 \
--hash=sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639
-tzlocal==5.2 \
- --hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \
- --hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e
+tzlocal==5.3 \
+ --hash=sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2 \
+ --hash=sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c
urllib3==2.3.0 \
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 32afa0d11..48a5a34b7 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -692,13 +692,16 @@ def from_terminal(cls, opts: TerminalOptions) -> Self:
config_dict[section][name] = res
another = True
+ # NOTE: Make sure that header is above other sections
config_dict = { # type: ignore[assignment]
'package': opts.package,
'spec_version': '3.0',
**config_dict,
}
- return cls(**config_dict) # type: ignore[arg-type]
+ self = cls(**config_dict) # type: ignore[arg-type]
+ self._json = config_dict # type: ignore[assignment]
+ return self
@classmethod
def load(
@@ -716,9 +719,6 @@ def load(
)
try:
- # from pydantic.dataclasses import rebuild_dataclass
- # rebuild_dataclass(cls, force=True)
-
config = TypeAdapter(cls).validate_python(config_json)
except ConfigurationError:
raise
diff --git a/uv.lock b/uv.lock
index 922958893..4372a6db0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -376,19 +376,21 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.6.11"
+version = "7.6.12"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/89/4e/38141d42af7452f4b7c5d3d7442a8018de34754ef52eb9a400768bc8d59e/coverage-7.6.11.tar.gz", hash = "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286", size = 805460 }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/65/83/cf3d6ac06bd02e1fb7fc6609d7a3be799328a94938dd2a64cf091989b8ce/coverage-7.6.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36", size = 208543 },
- { url = "https://files.pythonhosted.org/packages/e7/e1/b1448995072ab033898758179e208afa924f4625ea4524ec868fafbae77d/coverage-7.6.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea", size = 208805 },
- { url = "https://files.pythonhosted.org/packages/80/22/11ae7726086bf16ad35ecd1ebf31c0c709647b2618977bc088003bd38808/coverage-7.6.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce", size = 239768 },
- { url = "https://files.pythonhosted.org/packages/7d/68/717286bda6530f39f3ac16899dac1855a71921aca5ee565484269326c979/coverage-7.6.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297", size = 242023 },
- { url = "https://files.pythonhosted.org/packages/93/57/4b028c7c882411d9ca3f12cd4223ceeb5cb39f84bb91c4fb21a06440cbd9/coverage-7.6.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963", size = 239610 },
- { url = "https://files.pythonhosted.org/packages/44/88/720c9eba316406f243670237306bcdb8e269e4d0e12b191a697f66369404/coverage-7.6.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce", size = 241212 },
- { url = "https://files.pythonhosted.org/packages/1d/ae/a09edf77bd535d597de13679262845f5cb6ff1fab37a3065640fb3d5e6e8/coverage-7.6.11-cp312-cp312-win32.whl", hash = "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12", size = 211186 },
- { url = "https://files.pythonhosted.org/packages/80/5d/63ad5e3f1421504194da0228d259a3913884830999d1297b5e16b59bcb0f/coverage-7.6.11-cp312-cp312-win_amd64.whl", hash = "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551", size = 211974 },
- { url = "https://files.pythonhosted.org/packages/24/f3/63cd48409a519d4f6cf79abc6c89103a8eabc5c93e496f40779269dba0c0/coverage-7.6.11-py3-none-any.whl", hash = "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7", size = 200446 },
+ { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
+ { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
+ { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
+ { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
+ { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
+ { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
+ { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
+ { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
+ { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
+ { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
+ { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
]
[[package]]
@@ -444,7 +446,7 @@ wheels = [
[[package]]
name = "datamodel-code-generator"
-version = "0.27.2"
+version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "argcomplete" },
@@ -457,9 +459,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "pyyaml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8c/49/9cb4f868856304dd4e2fc0795d848889a7c9c6f2539165ad24977cef0da3/datamodel_code_generator-0.27.2.tar.gz", hash = "sha256:1a7655f5fd3a61329b57534904f5c40dd850850e420696fd946ec7a4f59c32b8", size = 436345 }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/d3/80f6a2394bbf3b46b150fc75afa5b0050f91baa5771e9be87df148013d83/datamodel_code_generator-0.28.1.tar.gz", hash = "sha256:37ef5f3b488f7d7a3f0b5b3ba0f2bc1ae01bab4dc7e0f6b99ff6c40713a6beb3", size = 434901 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/a0/678f10ecc40f1cce3c170246c3dd1b86735867d2844eb9f4596abf187dac/datamodel_code_generator-0.27.2-py3-none-any.whl", hash = "sha256:efcbfbe6a1488d3411fc588b1ce1af5f854f5107810b1cc9026a6d6333a7c4d8", size = 115483 },
+ { url = "https://files.pythonhosted.org/packages/c8/17/2876ca0a4ac7dd7cb5f56a2f0f6d9ac910969f467e8142c847c45a76b897/datamodel_code_generator-0.28.1-py3-none-any.whl", hash = "sha256:1ff8a56f9550a82bcba3e1ad7ebdb89bc655eeabbc4bc6acfb05977cbdc6381c", size = 115601 },
]
[[package]]
@@ -684,7 +686,7 @@ pycryptodome = [
[[package]]
name = "eth-keyfile"
-version = "0.9.0"
+version = "0.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "eth-keys" },
@@ -692,9 +694,9 @@ dependencies = [
{ name = "py-ecc" },
{ name = "pycryptodome" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6b/fc/1b957ce3403158417b8e632c87ceffb0c2b0845505c280571e23ae57bff3/eth_keyfile-0.9.0.tar.gz", hash = "sha256:8621c35e83cbc05909d2f23dbb8a87633918733caea553ae0e298f6a06291526", size = 19793 }
+sdist = { url = "https://files.pythonhosted.org/packages/08/e4/3f0c20b020786e1fa6e1ecd81806c54167fa2b0839e0020086b95a6e8faf/eth_keyfile-0.9.1.tar.gz", hash = "sha256:c7a8bc6af4527d1ab2eb1d1b949d59925252e17663eaf90087da121327b51df6", size = 19787 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/19/5f/854a4c2bb7184b69947b77ed1337009ad3e57e085dc7b2010dc13d868d4a/eth_keyfile-0.9.0-py3-none-any.whl", hash = "sha256:45d3513b6433ad885370225ba0429ed26493ba23589c5b1ca5da024765020fef", size = 9869 },
+ { url = "https://files.pythonhosted.org/packages/5f/08/9c8bf617b39e1dd56303593292e8b4eb66497a5f0f5b997a4b291e5343c0/eth_keyfile-0.9.1-py3-none-any.whl", hash = "sha256:9789c3b4fa0bb6e2616cdc2bdd71b8755b42947d78ef1e900a0149480fabb5c2", size = 9866 },
]
[[package]]
@@ -1066,20 +1068,20 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.2.2"
+version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/d0/c12ddfd3a02274be06ffc71f3efc6d0e457b0409c4481596881e748cb264/numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f", size = 20233295 }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/e6/847d15770ab7a01e807bdfcd4ead5bdae57c0092b7dc83878171b6af97bb/numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", size = 20912636 },
- { url = "https://files.pythonhosted.org/packages/d1/af/f83580891577b13bd7e261416120e036d0d8fb508c8a43a73e38928b794b/numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", size = 14098403 },
- { url = "https://files.pythonhosted.org/packages/2b/86/d019fb60a9d0f1d4cf04b014fe88a9135090adfadcc31c1fadbb071d7fa7/numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", size = 5128938 },
- { url = "https://files.pythonhosted.org/packages/7a/1b/50985edb6f1ec495a1c36452e860476f5b7ecdc3fc59ea89ccad3c4926c5/numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", size = 6661937 },
- { url = "https://files.pythonhosted.org/packages/f4/1b/17efd94cad1b9d605c3f8907fb06bcffc4ce4d1d14d46b95316cccccf2b9/numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", size = 14049518 },
- { url = "https://files.pythonhosted.org/packages/5b/73/65d2f0b698df1731e851e3295eb29a5ab8aa06f763f7e4188647a809578d/numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", size = 16099146 },
- { url = "https://files.pythonhosted.org/packages/d5/69/308f55c0e19d4b5057b5df286c5433822e3c8039ede06d4051d96f1c2c4e/numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", size = 15246336 },
- { url = "https://files.pythonhosted.org/packages/f0/d8/d8d333ad0d8518d077a21aeea7b7c826eff766a2b1ce1194dea95ca0bacf/numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", size = 17863507 },
- { url = "https://files.pythonhosted.org/packages/82/6e/0b84ad3103ffc16d6673e63b5acbe7901b2af96c2837174c6318c98e27ab/numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", size = 6276491 },
- { url = "https://files.pythonhosted.org/packages/fc/84/7f801a42a67b9772a883223a0a1e12069a14626c81a732bd70aac57aebc1/numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", size = 12616372 },
+ { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 },
+ { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 },
+ { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 },
+ { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 },
+ { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 },
+ { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 },
+ { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 },
+ { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 },
+ { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 },
]
[[package]]
@@ -1217,17 +1219,17 @@ wheels = [
[[package]]
name = "psutil"
-version = "6.1.1"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 },
- { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 },
- { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 },
- { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 },
- { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 },
- { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 },
- { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 },
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
]
[[package]]
@@ -1569,27 +1571,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.9.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 },
- { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 },
- { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 },
- { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 },
- { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 },
- { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 },
- { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 },
- { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 },
- { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 },
- { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 },
- { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 },
- { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 },
- { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 },
- { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 },
- { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 },
- { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 },
- { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 },
+version = "0.9.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
+ { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
+ { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
+ { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
+ { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
+ { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
+ { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
+ { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
+ { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
+ { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
+ { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
+ { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
+ { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
+ { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
+ { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
+ { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
]
[[package]]
@@ -1630,15 +1632,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
-version = "2.20.0"
+version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/68/e8/6a366c0cd5e129dda6ecb20ff097f70b18182c248d4c27e813c21f98992a/sentry_sdk-2.20.0.tar.gz", hash = "sha256:afa82713a92facf847df3c6f63cec71eb488d826a50965def3d7722aa6f0fdab", size = 300125 }
+sdist = { url = "https://files.pythonhosted.org/packages/08/63/3f0e88709cf4af992e2813c27d8ba628a891db0805e3fcc6dc834e142c5b/sentry_sdk-2.21.0.tar.gz", hash = "sha256:a6d38e0fb35edda191acf80b188ec713c863aaa5ad8d5798decb8671d02077b6", size = 301965 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/0f/6f7e6cd0f4a141752caef3f79300148422fdf2b8b68b531f30b2b0c0cbda/sentry_sdk-2.20.0-py2.py3-none-any.whl", hash = "sha256:c359a1edf950eb5e80cffd7d9111f3dbeef57994cb4415df37d39fda2cf22364", size = 322576 },
+ { url = "https://files.pythonhosted.org/packages/a4/18/7587660cb5e4d07134913d8e74137efcd4903fda873bf612c30eb34c7ab4/sentry_sdk-2.21.0-py2.py3-none-any.whl", hash = "sha256:7623cfa9e2c8150948a81ca253b8e2bfe4ce0b96ab12f8cd78e3ac9c490fd92f", size = 324096 },
]
[[package]]
@@ -1867,14 +1869,14 @@ wheels = [
[[package]]
name = "typeguard"
-version = "4.4.1"
+version = "4.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/62/c3/400917dd37d7b8c07e9723f3046818530423e1e759a56a22133362adab00/typeguard-4.4.1.tar.gz", hash = "sha256:0d22a89d00b453b47c49875f42b6601b961757541a2e1e0ef517b6e24213c21b", size = 74959 }
+sdist = { url = "https://files.pythonhosted.org/packages/70/60/8cd6a3d78d00ceeb2193c02b7ed08f063d5341ccdfb24df88e61f383048e/typeguard-4.4.2.tar.gz", hash = "sha256:a6f1065813e32ef365bc3b3f503af8a96f9dd4e0033a02c28c4a4983de8c6c49", size = 75746 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/53/9465dedf2d69fe26008e7732cf6e0a385e387c240869e7d54eed49782a3c/typeguard-4.4.1-py3-none-any.whl", hash = "sha256:9324ec07a27ec67fc54a9c063020ca4c0ae6abad5e9f0f9804ca59aee68c6e21", size = 35635 },
+ { url = "https://files.pythonhosted.org/packages/cf/4b/9a77dc721aa0b7f74440a42e4ef6f9a4fae7324e17f64f88b96f4c25cc05/typeguard-4.4.2-py3-none-any.whl", hash = "sha256:77a78f11f09777aeae7fa08585f33b5f4ef0e7335af40005b0c422ed398ff48c", size = 35801 },
]
[[package]]
@@ -1940,14 +1942,14 @@ wheels = [
[[package]]
name = "tzlocal"
-version = "5.2"
+version = "5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 }
+sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859 },
+ { url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920 },
]
[[package]]
From d51f7e6f6572e64946bc0821fa9c6809b96c25d2 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Mon, 17 Feb 2025 10:53:45 -0300
Subject: [PATCH 10/30] fix CI
---
src/dipdup/cli.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 5d054fee3..de16b8048 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -852,6 +852,8 @@ async def new(
from dipdup.project import render_project
from dipdup.project import template_from_terminal
+ config_dict: dict[str, Any] = {}
+
if quiet:
answers = get_default_answers()
if template:
@@ -867,7 +869,6 @@ async def new(
if template:
echo(f'Using template `{template}`\n')
- config_dict: dict[str, Any] | None = {}
else:
template, config_dict = template_from_terminal(answers['package'])
From 58e1c477b21905d9175d6b049d7b2ff199e71494 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Tue, 18 Feb 2025 07:12:40 -0300
Subject: [PATCH 11/30] default order
---
src/dipdup/cli.py | 2 +-
src/dipdup/project.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index de16b8048..9e210da44 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -852,7 +852,7 @@ async def new(
from dipdup.project import render_project
from dipdup.project import template_from_terminal
- config_dict: dict[str, Any] = {}
+ config_dict: dict[str, Any] | None = None
if quiet:
answers = get_default_answers()
diff --git a/src/dipdup/project.py b/src/dipdup/project.py
index ae5618053..5fa5eba4c 100644
--- a/src/dipdup/project.py
+++ b/src/dipdup/project.py
@@ -163,13 +163,13 @@ def template_from_terminal(package: str) -> tuple[str | None, dict[str, Any] | N
_, mode = prompt_anyof(
question='How would you like to set up your new DipDup project?',
options=(
- 'Interactively',
'From template',
+ 'Interactively',
'Blank',
),
comments=(
- 'Guided setup with prompts',
'Use one of demo projects',
+ 'Guided setup with prompts',
'Begin with an empty project',
),
default=0,
From 53d9a95a97c788524092fea9bfbd5624a175d791 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Wed, 19 Feb 2025 10:48:31 -0300
Subject: [PATCH 12/30] MCP WIP
---
indexer_state.py | 20 +++++
pyproject.toml | 7 +-
src/demo_tezos_auction/dipdup.yaml | 17 ++--
src/dipdup/api.py | 30 ++++++++
src/dipdup/cli.py | 30 ++++++++
src/dipdup/config/__init__.py | 13 ++++
src/dipdup/dipdup.py | 2 +
uv.lock | 120 ++++++++++++++++++++++++++++-
8 files changed, 227 insertions(+), 12 deletions(-)
create mode 100644 indexer_state.py
diff --git a/indexer_state.py b/indexer_state.py
new file mode 100644
index 000000000..f680d7f31
--- /dev/null
+++ b/indexer_state.py
@@ -0,0 +1,20 @@
+from headmcp import HeadMCP
+
+def get_indexer_state():
+ # Initialize Head MCP client
+ head_mcp = HeadMCP()
+
+ # Fetch the current indexer state
+ indexer_state = head_mcp.get_indexer_state()
+
+ return indexer_state
+
+def main():
+ try:
+ state = get_indexer_state()
+ print(f"Current Indexer State: {state}")
+ except Exception as e:
+ print(f"Error fetching indexer state: {e}")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 4a3776d53..b314eab87 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,6 +61,7 @@ dependencies = [
"datamodel-code-generator~=0.26",
"eth-abi~=5.0",
"lru-dict~=1.3",
+ "mcp>=1.2.1",
"orjson~=3.10",
"prometheus-client~=0.20",
"pycryptodome~=3.20",
@@ -68,15 +69,15 @@ dependencies = [
"pyhumps~=3.8",
"pysignalr~=1.0",
"python-dotenv~=1.0",
- "python-json-logger~=2.0", # pinned
+ "python-json-logger~=2.0", # pinned
"ruamel.yaml~=0.18.6",
"sentry-sdk~=2.16",
"sqlparse~=0.5",
- "starknet-py~=0.25.0", # pinned
+ "starknet-py~=0.25.0", # pinned
"strict-rfc3339~=0.7",
"survey~=5.4",
"tabulate~=0.9",
- "tortoise-orm==0.24.0", # pinned
+ "tortoise-orm==0.24.0", # pinned
"uvloop~=0.20",
"web3~=7.2",
]
diff --git a/src/demo_tezos_auction/dipdup.yaml b/src/demo_tezos_auction/dipdup.yaml
index 689bd930b..12f897282 100644
--- a/src/demo_tezos_auction/dipdup.yaml
+++ b/src/demo_tezos_auction/dipdup.yaml
@@ -1,3 +1,6 @@
+mcp: {}
+api: {}
+
spec_version: 3.0
package: demo_tezos_auction
@@ -40,10 +43,10 @@ templates:
destination:
entrypoint: withdraw
-indexes:
- tzcolors:
- template: auction
- values:
- datasource: tzkt
- minter: tzcolors_minter
- auction: tzcolors_auction
\ No newline at end of file
+# indexes:
+# tzcolors:
+# template: auction
+# values:
+# datasource: tzkt
+# minter: tzcolors_minter
+# auction: tzcolors_auction
\ No newline at end of file
diff --git a/src/dipdup/api.py b/src/dipdup/api.py
index 95fe5d5c2..3dd2a5c7b 100644
--- a/src/dipdup/api.py
+++ b/src/dipdup/api.py
@@ -2,6 +2,7 @@
from collections.abc import Awaitable
from collections.abc import Callable
from json import JSONDecodeError
+from typing import TYPE_CHECKING
import orjson
from aiohttp import web
@@ -12,6 +13,9 @@
from dipdup.utils import json_dumps
+if TYPE_CHECKING:
+ from mcp.server.fastmcp import FastMCP
+
def _method_wrapper(
ctx: 'DipDupContext',
method: Callable[[DipDupContext, web.Request], Awaitable[web.Response]],
@@ -48,6 +52,32 @@ async def _performance(ctx: 'DipDupContext', request: web.Request) -> web.Respon
dumps=lambda x: json_dumps(x, option=orjson.OPT_SORT_KEYS).decode(),
)
+async def create_mcp() -> 'FastMCP':
+ from mcp.server.fastmcp import FastMCP
+ from dipdup import models
+
+ mcp = FastMCP('DipDup', port=9999)
+
+ @mcp.tool(name='Indexes', description='Fetch the current state of the indexer')
+ async def indexes() -> str:
+ index = await models.Index.filter().limit(1).get()
+ return str(index.__dict__)
+
+
+ @mcp.tool(name='Head', description='Fetch the current head block')
+ async def head() -> str:
+ res = ''
+ for m in (await models.Head.all()):
+ res += f"""
+Datasource name: {m.name}
+Current height: {m.level}
+Block hash: {m.hash}
+Block timestamp: {m.timestamp}
+"""
+
+ return res
+
+ return mcp
async def create_api(ctx: DipDupContext) -> web.Application:
routes = web.RouteTableDef()
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 9e210da44..9e917be86 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -3,6 +3,7 @@
import atexit
import logging
import sys
+from threading import Thread
import traceback
from collections import defaultdict
from collections.abc import Callable
@@ -529,6 +530,35 @@ async def hasura(ctx: click.Context) -> None:
pass
+@cli.group(help='Commands related to MCP integration.')
+@click.pass_context
+@_cli_wrapper
+async def mcp(ctx: click.Context) -> None:
+ pass
+
+
+@mcp.command(name='run')
+@click.pass_context
+@_cli_wrapper
+async def mcp_run(ctx: click.Context) -> None:
+ """Run MCP server."""
+
+ from dipdup.api import create_mcp
+
+ from anyio import run, from_thread
+
+ mcp = await create_mcp()
+
+
+ # async def main():
+ with from_thread.start_blocking_portal() as portal:
+ portal.call(mcp.run_sse_async)
+
+ # thread = Thread(target=mcp.run)
+ # thread.run()
+ # config: DipDupConfig = ctx.obj.config
+ # run_mcp(config)
+
@hasura.command(name='configure')
@click.option('--force', '-f', is_flag=True, help='Proceed even if Hasura is already configured.')
@click.pass_context
diff --git a/src/dipdup/config/__init__.py b/src/dipdup/config/__init__.py
index 48a5a34b7..64f98780b 100644
--- a/src/dipdup/config/__init__.py
+++ b/src/dipdup/config/__init__.py
@@ -545,6 +545,18 @@ class ApiConfig:
port: int = 46339 # dial INDEX 😎
+@dataclass(config=ConfigDict(extra='forbid', defer_build=True), kw_only=True)
+class McpConfig:
+ """Config for MCP integration.
+
+ :param host: Host to bind to
+ :param port: Port to bind to
+ """
+
+ host: str = '127.0.0.1'
+ port: int = 9999
+
+
# NOTE: Should be the only place where extras are allowed
@dataclass(config=ConfigDict(extra='allow', defer_build=True), kw_only=True)
class AdvancedConfig:
@@ -610,6 +622,7 @@ class DipDupConfig(InteractiveMixin):
advanced: AdvancedConfig = Field(default_factory=AdvancedConfig)
custom: dict[str, Any] = Field(default_factory=dict)
logging: dict[str, str | int] | str | int = 'INFO'
+ mcp: McpConfig | None = None
def __post_init__(self) -> None:
if self.package != pascal_to_snake(self.package):
diff --git a/src/dipdup/dipdup.py b/src/dipdup/dipdup.py
index 01ea78a9c..e5ce8d0c5 100644
--- a/src/dipdup/dipdup.py
+++ b/src/dipdup/dipdup.py
@@ -1,5 +1,6 @@
import asyncio
import logging
+from threading import Thread
import time
from asyncio import CancelledError
from asyncio import Event
@@ -715,6 +716,7 @@ async def run(self) -> None:
await self._set_up_hooks()
await self._set_up_prometheus()
await self._set_up_api(stack)
+ # await self._set_up_mcp(tasks)
await self._initialize_schema()
await self._initialize_migrations()
diff --git a/uv.lock b/uv.lock
index 4372a6db0..52faccc29 100644
--- a/uv.lock
+++ b/uv.lock
@@ -490,6 +490,7 @@ dependencies = [
{ name = "datamodel-code-generator" },
{ name = "eth-abi" },
{ name = "lru-dict" },
+ { name = "mcp" },
{ name = "orjson" },
{ name = "prometheus-client" },
{ name = "pycryptodome" },
@@ -556,6 +557,7 @@ requires-dist = [
{ name = "datamodel-code-generator", specifier = "~=0.26" },
{ name = "eth-abi", specifier = "~=5.0" },
{ name = "lru-dict", specifier = "~=1.3" },
+ { name = "mcp", specifier = ">=1.2.1" },
{ name = "orjson", specifier = "~=3.10" },
{ name = "prometheus-client", specifier = "~=0.20" },
{ name = "pycryptodome", specifier = "~=3.20" },
@@ -795,6 +797,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470 },
]
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
[[package]]
name = "hexbytes"
version = "1.3.0"
@@ -804,6 +815,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/96/035871b535a728700d3cc5b94cf883706f345c5a088253f26f0bee0b7939/hexbytes-1.3.0-py3-none-any.whl", hash = "sha256:83720b529c6e15ed21627962938dc2dec9bb1010f17bbbd66bf1e6a8287d522c", size = 4902 },
]
+[[package]]
+name = "httpcore"
+version = "1.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
+]
+
[[package]]
name = "idna"
version = "3.10"
@@ -968,6 +1016,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/81/3ef15337c19d3e3432945aad738081a5f54c16885277c7dff300b5f85b24/marshmallow_oneofschema-3.1.1-py3-none-any.whl", hash = "sha256:ff4cb2a488785ee8edd521a765682c2c80c78b9dc48894124531bdfa1ec9303b", size = 5726 },
]
+[[package]]
+name = "mcp"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/30/51e4555826126e3954fa2ab1e934bf74163c5fe05e98f38ca4d0f8abbf63/mcp-1.2.1.tar.gz", hash = "sha256:c9d43dbfe943aa1530e2be8f54b73af3ebfb071243827b4483d421684806cb45", size = 103968 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/0d/6770742a84c8aa1d36c0d628896a380584c5759612e66af7446af07d8775/mcp-1.2.1-py3-none-any.whl", hash = "sha256:579bf9c9157850ebb1344f3ca6f7a3021b0123c44c9f089ef577a7062522f0fd", size = 66453 },
+]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -1262,8 +1329,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 },
{ url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 },
{ url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 },
- { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 },
- { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 },
]
[[package]]
@@ -1305,6 +1370,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
]
+[[package]]
+name = "pydantic-settings"
+version = "2.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 },
+]
+
[[package]]
name = "pygments"
version = "2.19.1"
@@ -1788,6 +1866,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 },
]
+[[package]]
+name = "sse-starlette"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
+]
+
[[package]]
name = "starknet-py"
version = "0.25.0"
@@ -1807,6 +1898,18 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/36/ea/2ec249b38dccd8391b714a0dce2172d6298d7e043b063e529d897622f88d/starknet_py-0.25.0.tar.gz", hash = "sha256:452f2c9ed5e235540209ec2042d22dcf79c6cec2a6850f5b518d61b3fada3706", size = 97660 }
+[[package]]
+name = "starlette"
+version = "0.45.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
+]
+
[[package]]
name = "strict-rfc3339"
version = "0.7"
@@ -1961,6 +2064,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
+[[package]]
+name = "uvicorn"
+version = "0.34.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
+]
+
[[package]]
name = "uvloop"
version = "0.21.0"
From 18205c91e9ea0f6bb153fee5adc8f589e9c47b3f Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 20 Feb 2025 08:41:11 -0300
Subject: [PATCH 13/30] wip
---
src/dipdup/api.py | 23 ++++++++++++++++++-----
src/dipdup/cli.py | 16 +++++++---------
src/dipdup/dipdup.py | 1 -
3 files changed, 25 insertions(+), 15 deletions(-)
diff --git a/src/dipdup/api.py b/src/dipdup/api.py
index 3dd2a5c7b..6c1c2d7e7 100644
--- a/src/dipdup/api.py
+++ b/src/dipdup/api.py
@@ -8,14 +8,15 @@
from aiohttp import web
import dipdup.performance
+from dipdup.config import McpConfig
from dipdup.context import DipDupContext
from dipdup.exceptions import Error
from dipdup.utils import json_dumps
-
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
+
def _method_wrapper(
ctx: 'DipDupContext',
method: Callable[[DipDupContext, web.Request], Awaitable[web.Response]],
@@ -52,22 +53,33 @@ async def _performance(ctx: 'DipDupContext', request: web.Request) -> web.Respon
dumps=lambda x: json_dumps(x, option=orjson.OPT_SORT_KEYS).decode(),
)
-async def create_mcp() -> 'FastMCP':
+
+async def create_mcp(config: McpConfig) -> 'FastMCP':
+ import mcp.server.fastmcp.utilities.logging
+
+ mcp.server.fastmcp.utilities.logging.configure_logging = lambda _: None
+
from mcp.server.fastmcp import FastMCP
+
from dipdup import models
- mcp = FastMCP('DipDup', port=9999)
+ mcp = FastMCP(
+ 'DipDup',
+ host=config.host,
+ port=config.port,
+ debug=True,
+ log_level='DEBUG',
+ )
@mcp.tool(name='Indexes', description='Fetch the current state of the indexer')
async def indexes() -> str:
index = await models.Index.filter().limit(1).get()
return str(index.__dict__)
-
@mcp.tool(name='Head', description='Fetch the current head block')
async def head() -> str:
res = ''
- for m in (await models.Head.all()):
+ for m in await models.Head.all():
res += f"""
Datasource name: {m.name}
Current height: {m.level}
@@ -79,6 +91,7 @@ async def head() -> str:
return mcp
+
async def create_api(ctx: DipDupContext) -> web.Application:
routes = web.RouteTableDef()
routes.get('/')(_method_wrapper(ctx, _performance))
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 9e917be86..3d9a0f778 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -3,7 +3,6 @@
import atexit
import logging
import sys
-from threading import Thread
import traceback
from collections import defaultdict
from collections.abc import Callable
@@ -543,21 +542,20 @@ async def mcp(ctx: click.Context) -> None:
async def mcp_run(ctx: click.Context) -> None:
"""Run MCP server."""
- from dipdup.api import create_mcp
+ from anyio import from_thread
- from anyio import run, from_thread
+ from dipdup.api import create_mcp
+ from dipdup.exceptions import ConfigurationError
- mcp = await create_mcp()
+ config: DipDupConfig = ctx.obj.config
+ if not config.mcp:
+ raise ConfigurationError('`mcp` config section is empty')
+ mcp = await create_mcp(config.mcp)
- # async def main():
with from_thread.start_blocking_portal() as portal:
portal.call(mcp.run_sse_async)
- # thread = Thread(target=mcp.run)
- # thread.run()
- # config: DipDupConfig = ctx.obj.config
- # run_mcp(config)
@hasura.command(name='configure')
@click.option('--force', '-f', is_flag=True, help='Proceed even if Hasura is already configured.')
diff --git a/src/dipdup/dipdup.py b/src/dipdup/dipdup.py
index e5ce8d0c5..768156b0c 100644
--- a/src/dipdup/dipdup.py
+++ b/src/dipdup/dipdup.py
@@ -1,6 +1,5 @@
import asyncio
import logging
-from threading import Thread
import time
from asyncio import CancelledError
from asyncio import Event
From 09f06b85ece2b0cdf91537d6f82656f19bd69d7c Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 20 Feb 2025 10:55:10 -0300
Subject: [PATCH 14/30] it's working
---
src/demo_evm_events/dipdup.yaml | 4 +++-
src/dipdup/api.py | 13 ++++++++++---
src/dipdup/cli.py | 10 +++++++++-
src/dipdup/dipdup.py | 1 -
4 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/src/demo_evm_events/dipdup.yaml b/src/demo_evm_events/dipdup.yaml
index d14101c55..cedf4f9c9 100644
--- a/src/demo_evm_events/dipdup.yaml
+++ b/src/demo_evm_events/dipdup.yaml
@@ -30,4 +30,6 @@ indexes:
handlers:
- callback: on_transfer
contract: eth_usdt
- name: Transfer
\ No newline at end of file
+ name: Transfer
+
+mcp: {}
\ No newline at end of file
diff --git a/src/dipdup/api.py b/src/dipdup/api.py
index 6c1c2d7e7..e3fd0259c 100644
--- a/src/dipdup/api.py
+++ b/src/dipdup/api.py
@@ -73,10 +73,17 @@ async def create_mcp(config: McpConfig) -> 'FastMCP':
@mcp.tool(name='Indexes', description='Fetch the current state of the indexer')
async def indexes() -> str:
- index = await models.Index.filter().limit(1).get()
- return str(index.__dict__)
+ res = ''
+ for m in await models.Index.all():
+ res += f"""
+Index name: {m.name}
+Type: {m.type}
+Status: {m.status}
+Current height: {m.level}
+"""
+ return res
- @mcp.tool(name='Head', description='Fetch the current head block')
+ @mcp.tool(name='Heads', description='Fetch the current datasource head blocks')
async def head() -> str:
res = ''
for m in await models.Head.all():
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 3d9a0f778..b573edc42 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -545,6 +545,8 @@ async def mcp_run(ctx: click.Context) -> None:
from anyio import from_thread
from dipdup.api import create_mcp
+ from dipdup.config import DipDupConfig
+ from dipdup.database import tortoise_wrapper
from dipdup.exceptions import ConfigurationError
config: DipDupConfig = ctx.obj.config
@@ -554,7 +556,13 @@ async def mcp_run(ctx: click.Context) -> None:
mcp = await create_mcp(config.mcp)
with from_thread.start_blocking_portal() as portal:
- portal.call(mcp.run_sse_async)
+ async with tortoise_wrapper(
+ url=config.database.connection_string,
+ models=config.package,
+ timeout=config.database.connection_timeout,
+ ):
+ # await portal.spawn(mcp.run)
+ portal.call(mcp.run_sse_async)
@hasura.command(name='configure')
diff --git a/src/dipdup/dipdup.py b/src/dipdup/dipdup.py
index 768156b0c..01ea78a9c 100644
--- a/src/dipdup/dipdup.py
+++ b/src/dipdup/dipdup.py
@@ -715,7 +715,6 @@ async def run(self) -> None:
await self._set_up_hooks()
await self._set_up_prometheus()
await self._set_up_api(stack)
- # await self._set_up_mcp(tasks)
await self._initialize_schema()
await self._initialize_migrations()
From 909a92215ea006ca6a0027793da99448f606fff9 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 20 Feb 2025 11:26:24 -0300
Subject: [PATCH 15/30] ref
---
src/dipdup/api.py | 17 +++++++++++++----
src/dipdup/cli.py | 19 +++++++------------
2 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/src/dipdup/api.py b/src/dipdup/api.py
index e3fd0259c..8b70989e3 100644
--- a/src/dipdup/api.py
+++ b/src/dipdup/api.py
@@ -8,8 +8,8 @@
from aiohttp import web
import dipdup.performance
-from dipdup.config import McpConfig
from dipdup.context import DipDupContext
+from dipdup.exceptions import ConfigurationError
from dipdup.exceptions import Error
from dipdup.utils import json_dumps
@@ -54,7 +54,7 @@ async def _performance(ctx: 'DipDupContext', request: web.Request) -> web.Respon
)
-async def create_mcp(config: McpConfig) -> 'FastMCP':
+async def create_mcp(ctx: DipDupContext) -> 'FastMCP':
import mcp.server.fastmcp.utilities.logging
mcp.server.fastmcp.utilities.logging.configure_logging = lambda _: None
@@ -63,14 +63,23 @@ async def create_mcp(config: McpConfig) -> 'FastMCP':
from dipdup import models
+ if not (mcp_config := ctx.config.mcp):
+ raise ConfigurationError('MCP config is not provided')
+
mcp = FastMCP(
'DipDup',
- host=config.host,
- port=config.port,
+ host=mcp_config.host,
+ port=mcp_config.port,
+ # FIXME: both not working
debug=True,
log_level='DEBUG',
)
+ @mcp.tool(name='Config', description='Describe the current configuration')
+ async def config() -> str:
+ # FIXME: strip secrets
+ return ctx.config.dump()
+
@mcp.tool(name='Indexes', description='Fetch the current state of the indexer')
async def indexes() -> str:
res = ''
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index b573edc42..2f60be18e 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -546,22 +546,17 @@ async def mcp_run(ctx: click.Context) -> None:
from dipdup.api import create_mcp
from dipdup.config import DipDupConfig
- from dipdup.database import tortoise_wrapper
- from dipdup.exceptions import ConfigurationError
+ from dipdup.dipdup import DipDup
config: DipDupConfig = ctx.obj.config
- if not config.mcp:
- raise ConfigurationError('`mcp` config section is empty')
-
- mcp = await create_mcp(config.mcp)
+ dipdup = DipDup(config)
+ mcp = await create_mcp(dipdup._ctx)
with from_thread.start_blocking_portal() as portal:
- async with tortoise_wrapper(
- url=config.database.connection_string,
- models=config.package,
- timeout=config.database.connection_timeout,
- ):
- # await portal.spawn(mcp.run)
+ async with AsyncExitStack() as stack:
+ await dipdup._create_datasources()
+ await dipdup._set_up_database(stack)
+
portal.call(mcp.run_sse_async)
From ed51b18661de32a534c7c23df71ad4f7f475f8d9 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Thu, 20 Feb 2025 13:53:54 -0300
Subject: [PATCH 16/30] ref, user example
---
src/demo_evm_events/__init__.py | 12 +++++
src/dipdup/api.py | 59 ---------------------
src/dipdup/cli.py | 11 +++-
src/dipdup/mcp.py | 93 +++++++++++++++++++++++++++++++++
4 files changed, 114 insertions(+), 61 deletions(-)
create mode 100644 src/dipdup/mcp.py
diff --git a/src/demo_evm_events/__init__.py b/src/demo_evm_events/__init__.py
index e69de29bb..905c01a96 100644
--- a/src/demo_evm_events/__init__.py
+++ b/src/demo_evm_events/__init__.py
@@ -0,0 +1,12 @@
+from dipdup import mcp
+
+from demo_evm_events import models
+
+
+@mcp.tool('TopHolders', 'Top holders stats')
+async def tool_holders() -> str:
+ holders = await models.Holder.filter().order_by('-balance').limit(10).all()
+ res = 'Top holders:\n'
+ for holder in holders:
+ res += f'{holder.address}: {holder.balance}\n'
+ return res
diff --git a/src/dipdup/api.py b/src/dipdup/api.py
index 8b70989e3..95fe5d5c2 100644
--- a/src/dipdup/api.py
+++ b/src/dipdup/api.py
@@ -2,20 +2,15 @@
from collections.abc import Awaitable
from collections.abc import Callable
from json import JSONDecodeError
-from typing import TYPE_CHECKING
import orjson
from aiohttp import web
import dipdup.performance
from dipdup.context import DipDupContext
-from dipdup.exceptions import ConfigurationError
from dipdup.exceptions import Error
from dipdup.utils import json_dumps
-if TYPE_CHECKING:
- from mcp.server.fastmcp import FastMCP
-
def _method_wrapper(
ctx: 'DipDupContext',
@@ -54,60 +49,6 @@ async def _performance(ctx: 'DipDupContext', request: web.Request) -> web.Respon
)
-async def create_mcp(ctx: DipDupContext) -> 'FastMCP':
- import mcp.server.fastmcp.utilities.logging
-
- mcp.server.fastmcp.utilities.logging.configure_logging = lambda _: None
-
- from mcp.server.fastmcp import FastMCP
-
- from dipdup import models
-
- if not (mcp_config := ctx.config.mcp):
- raise ConfigurationError('MCP config is not provided')
-
- mcp = FastMCP(
- 'DipDup',
- host=mcp_config.host,
- port=mcp_config.port,
- # FIXME: both not working
- debug=True,
- log_level='DEBUG',
- )
-
- @mcp.tool(name='Config', description='Describe the current configuration')
- async def config() -> str:
- # FIXME: strip secrets
- return ctx.config.dump()
-
- @mcp.tool(name='Indexes', description='Fetch the current state of the indexer')
- async def indexes() -> str:
- res = ''
- for m in await models.Index.all():
- res += f"""
-Index name: {m.name}
-Type: {m.type}
-Status: {m.status}
-Current height: {m.level}
-"""
- return res
-
- @mcp.tool(name='Heads', description='Fetch the current datasource head blocks')
- async def head() -> str:
- res = ''
- for m in await models.Head.all():
- res += f"""
-Datasource name: {m.name}
-Current height: {m.level}
-Block hash: {m.hash}
-Block timestamp: {m.timestamp}
-"""
-
- return res
-
- return mcp
-
-
async def create_api(ctx: DipDupContext) -> web.Application:
routes = web.RouteTableDef()
routes.get('/')(_method_wrapper(ctx, _performance))
diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py
index 2f60be18e..77e6f933c 100644
--- a/src/dipdup/cli.py
+++ b/src/dipdup/cli.py
@@ -214,6 +214,7 @@ def _skip_cli_group() -> bool:
['hasura'],
['package'],
['schema'],
+ ['mcp'],
)
# NOTE: Simple helpers that don't use any of our cli boilerplate
is_script_group = args[0] in (
@@ -544,13 +545,19 @@ async def mcp_run(ctx: click.Context) -> None:
from anyio import from_thread
- from dipdup.api import create_mcp
from dipdup.config import DipDupConfig
from dipdup.dipdup import DipDup
+ from dipdup.mcp import configure_mcp
+ from dipdup.mcp import get_mcp
config: DipDupConfig = ctx.obj.config
dipdup = DipDup(config)
- mcp = await create_mcp(dipdup._ctx)
+
+ mcp = get_mcp()
+ configure_mcp(dipdup._ctx)
+
+ # NOTE: Import all submodules to find @mcp.tool decorators
+ dipdup._ctx.package.verify()
with from_thread.start_blocking_portal() as portal:
async with AsyncExitStack() as stack:
diff --git a/src/dipdup/mcp.py b/src/dipdup/mcp.py
new file mode 100644
index 000000000..8839d976c
--- /dev/null
+++ b/src/dipdup/mcp.py
@@ -0,0 +1,93 @@
+from typing import TYPE_CHECKING, Callable
+
+from dipdup import models
+from dipdup.context import DipDupContext
+
+if TYPE_CHECKING:
+ from mcp.server.fastmcp import FastMCP
+
+_mcp = None
+_ctx = None
+
+
+async def _tool_config() -> str:
+ # FIXME: strip secrets
+ return _ctx.config.dump()
+
+
+async def _tool_indexes() -> str:
+ res = ''
+ for m in await models.Index.all():
+ res += f"""
+Index name: {m.name}
+Type: {m.type}
+Status: {m.status}
+Current height: {m.level}
+"""
+ return res
+
+
+async def _tool_heads() -> str:
+ res = ''
+ for m in await models.Head.all():
+ res += f"""
+Datasource name: {m.name}
+Current height: {m.level}
+Block hash: {m.hash}
+Block timestamp: {m.timestamp}
+"""
+
+ return res
+
+
+def _create_mcp() -> 'FastMCP':
+ from mcp.server.fastmcp import FastMCP
+
+ mcp = FastMCP(
+ 'DipDup',
+ # FIXME: both not working
+ debug=True,
+ log_level='DEBUG',
+ )
+
+ # NOTE: Internal tools
+
+ mcp.tool(
+ name='Config',
+ description='Describe the current configuration',
+ )(_tool_config)
+
+ mcp.tool(
+ name='Indexes',
+ description='Fetch the current state of the indexer',
+ )(_tool_indexes)
+
+ mcp.tool(
+ name='Heads',
+ description='Fetch the current datasource head blocks',
+ )(_tool_heads)
+
+ return mcp
+
+
+def configure_mcp(ctx: DipDupContext) -> None:
+ global _ctx
+ _ctx = ctx
+
+ if mcp_config := ctx.config.mcp:
+ mcp = get_mcp()
+ mcp.settings.host = mcp_config.host
+ mcp.settings.port = mcp_config.port
+
+
+def get_mcp() -> 'FastMCP':
+ global _mcp
+
+ if not _mcp:
+ _mcp = _create_mcp()
+
+ return _mcp
+
+
+def tool(name: str, description: str) -> Callable:
+ return get_mcp().tool(name=name, description=description)
From 1119154e3e416e7a3e3b91d7e3c507b59bd15588 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Mon, 24 Feb 2025 11:46:45 -0300
Subject: [PATCH 17/30] templates
---
src/dipdup/mcp.py | 11 +++++++----
.../projects/base/configs/dipdup.compose.yaml.j2 | 6 ++++++
.../projects/base/configs/dipdup.sqlite.yaml.j2 | 6 ++++++
src/dipdup/projects/base/configs/dipdup.swarm.yaml.j2 | 6 ++++++
4 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/src/dipdup/mcp.py b/src/dipdup/mcp.py
index 8839d976c..b467a1d3c 100644
--- a/src/dipdup/mcp.py
+++ b/src/dipdup/mcp.py
@@ -1,4 +1,5 @@
-from typing import TYPE_CHECKING, Callable
+from collections.abc import Callable
+from typing import TYPE_CHECKING
from dipdup import models
from dipdup.context import DipDupContext
@@ -6,11 +7,12 @@
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
-_mcp = None
-_ctx = None
+_mcp: 'FastMCP | None' = None
+_ctx: DipDupContext | None = None
async def _tool_config() -> str:
+ assert _ctx
# FIXME: strip secrets
return _ctx.config.dump()
@@ -89,5 +91,6 @@ def get_mcp() -> 'FastMCP':
return _mcp
-def tool(name: str, description: str) -> Callable:
+def tool(name: str, description: str) -> Callable[..., None]:
+ assert ' ' not in name, 'Tool name should not contain spaces'
return get_mcp().tool(name=name, description=description)
diff --git a/src/dipdup/projects/base/configs/dipdup.compose.yaml.j2 b/src/dipdup/projects/base/configs/dipdup.compose.yaml.j2
index c17df5459..6cf9afed6 100644
--- a/src/dipdup/projects/base/configs/dipdup.compose.yaml.j2
+++ b/src/dipdup/projects/base/configs/dipdup.compose.yaml.j2
@@ -19,6 +19,12 @@ sentry:
prometheus:
host: 0.0.0.0
+ port: 8000
api:
host: 0.0.0.0
+ port: 46339
+
+mcp:
+ host: 0.0.0.0
+ port: 9999
diff --git a/src/dipdup/projects/base/configs/dipdup.sqlite.yaml.j2 b/src/dipdup/projects/base/configs/dipdup.sqlite.yaml.j2
index cfacbfb9a..cb24fa0e2 100644
--- a/src/dipdup/projects/base/configs/dipdup.sqlite.yaml.j2
+++ b/src/dipdup/projects/base/configs/dipdup.sqlite.yaml.j2
@@ -8,6 +8,12 @@ sentry:
prometheus:
host: 0.0.0.0
+ port: 8000
api:
host: 0.0.0.0
+ port: 46339
+
+mcp:
+ host: 0.0.0.0
+ port: 9999
diff --git a/src/dipdup/projects/base/configs/dipdup.swarm.yaml.j2 b/src/dipdup/projects/base/configs/dipdup.swarm.yaml.j2
index 39eb329fb..a2b387838 100644
--- a/src/dipdup/projects/base/configs/dipdup.swarm.yaml.j2
+++ b/src/dipdup/projects/base/configs/dipdup.swarm.yaml.j2
@@ -19,6 +19,12 @@ sentry:
prometheus:
host: 0.0.0.0
+ port: 8000
api:
host: 0.0.0.0
+ port: 46339
+
+mcp:
+ host: 0.0.0.0
+ port: 9999
From dd578233e4599737dffcafe27a2367081506c800 Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Mon, 17 Mar 2025 09:21:51 -0300
Subject: [PATCH 18/30] WIP
---
docs/7.references/1.cli.md | 18 +
docs/7.references/2.config.md | 129 +++----
docs/7.references/3.context.md | 26 +-
docs/7.references/4.models.md | 98 +++---
docs/9.release-notes/1.v8.3.md | 13 +
.../9.release-notes/{9.v7.0.md => 10.v7.0.md} | 0
docs/9.release-notes/{1.v8.2.md => 2.v8.2.md} | 0
docs/9.release-notes/{2.v8.1.md => 3.v8.1.md} | 0
docs/9.release-notes/{3.v8.0.md => 4.v8.0.md} | 0
docs/9.release-notes/{4.v7.5.md => 5.v7.5.md} | 0
docs/9.release-notes/{5.v7.4.md => 6.v7.4.md} | 0
docs/9.release-notes/{6.v7.3.md => 7.v7.3.md} | 0
docs/9.release-notes/{7.v7.2.md => 8.v7.2.md} | 0
docs/9.release-notes/{8.v7.1.md => 9.v7.1.md} | 0
docs/9.release-notes/_8.3_changelog.md | 0
indexer_state.py | 20 --
requirements.txt | 262 +++++++-------
.../handlers/factory/pool_created.py | 7 +-
src/demo_evm_uniswap/models/abi.py | 2 +-
src/demo_evm_uniswap/models/repo.py | 7 +-
.../handlers/on_update_expiry_map.py | 2 +-
.../hooks/check_expiration.py | 7 +-
.../handlers/on_factory_origination.py | 4 +-
src/dipdup/_version.py | 4 +-
src/dipdup/cli.py | 4 +-
src/dipdup/codegen/evm.py | 2 +-
src/dipdup/codegen/starknet.py | 2 +-
src/dipdup/codegen/substrate.py | 2 +-
src/dipdup/codegen/tezos.py | 8 +-
src/dipdup/config/__init__.py | 4 +-
src/dipdup/config/_mixin.py | 2 +-
src/dipdup/config/substrate_events.py | 2 +-
src/dipdup/config/tezos_operations.py | 4 +-
src/dipdup/database.py | 2 +-
src/dipdup/datasources/_subsquid.py | 2 +-
src/dipdup/datasources/coinbase.py | 2 +-
src/dipdup/datasources/evm_etherscan.py | 4 +-
src/dipdup/datasources/substrate_subscan.py | 4 +-
src/dipdup/datasources/tezos_tzkt.py | 22 +-
src/dipdup/datasources/tzip_metadata.py | 4 +-
src/dipdup/hasura.py | 6 +-
src/dipdup/index.py | 2 +-
src/dipdup/indexes/tezos_operations/parser.py | 4 +-
src/dipdup/install.py | 2 +-
src/dipdup/mcp.py | 137 ++++++--
src/dipdup/models/__init__.py | 7 +-
src/dipdup/models/substrate.py | 2 +-
src/dipdup/package.py | 2 +-
src/dipdup/performance.py | 4 +-
tests/test_index/test_tzkt_operations.py | 4 +-
uv.lock | 319 +++++++++---------
51 files changed, 640 insertions(+), 518 deletions(-)
create mode 100644 docs/9.release-notes/1.v8.3.md
rename docs/9.release-notes/{9.v7.0.md => 10.v7.0.md} (100%)
rename docs/9.release-notes/{1.v8.2.md => 2.v8.2.md} (100%)
rename docs/9.release-notes/{2.v8.1.md => 3.v8.1.md} (100%)
rename docs/9.release-notes/{3.v8.0.md => 4.v8.0.md} (100%)
rename docs/9.release-notes/{4.v7.5.md => 5.v7.5.md} (100%)
rename docs/9.release-notes/{5.v7.4.md => 6.v7.4.md} (100%)
rename docs/9.release-notes/{6.v7.3.md => 7.v7.3.md} (100%)
rename docs/9.release-notes/{7.v7.2.md => 8.v7.2.md} (100%)
rename docs/9.release-notes/{8.v7.1.md => 9.v7.1.md} (100%)
create mode 100644 docs/9.release-notes/_8.3_changelog.md
delete mode 100644 indexer_state.py
diff --git a/docs/7.references/1.cli.md b/docs/7.references/1.cli.md
index fb320104b..d2a29d5af 100644
--- a/docs/7.references/1.cli.md
+++ b/docs/7.references/1.cli.md
@@ -203,6 +203,24 @@ Discord: dipdup mcp [OPTIONS] COMMAND [ARGS]...
+
+
+
+
+### run
+
+Run MCP server.
+dipdup mcp run [OPTIONS]
+
+
+
+
+
+
## migrate
Migrate project to the new spec version.
diff --git a/docs/7.references/2.config.md b/docs/7.references/2.config.md
index e41959e14..e755ae1b9 100644
--- a/docs/7.references/2.config.md
+++ b/docs/7.references/2.config.md
@@ -10,7 +10,7 @@ description: "Config file reference"
## dipdup.config.DipDupConfig
-class dipdup.config.DipDupConfig(*args, spec_version, package, datasources=<factory>, database=<factory>, runtimes=<factory>, contracts=<factory>, indexes=<factory>, templates=<factory>, jobs=<factory>, hooks=<factory>, hasura=None, sentry=None, prometheus=None, api=None, advanced=<factory>, custom=<factory>, logging='INFO')
+class dipdup.config.DipDupConfig(*args, spec_version, package, datasources=<factory>, database=<factory>, runtimes=<factory>, contracts=<factory>, indexes=<factory>, templates=<factory>, jobs=<factory>, hooks=<factory>, hasura=None, sentry=None, prometheus=None, api=None, advanced=<factory>, custom=<factory>, logging='INFO', mcp=None)
DipDup project configuration file
- Parameters:
@@ -33,6 +33,7 @@ description: "Config file reference"
custom (dict[str, Any]) – User-defined configuration to use in callbacks
logging (dict[str, str | int] | str | int) – Modify logging verbosity
+mcp (McpConfig | None)
@@ -50,7 +51,7 @@ description: "Config file reference"
## dipdup.config.ContractConfig
-class dipdup.config.ContractConfig(**kwargs)
+class dipdup.config.ContractConfig(**kwargs)
Contract config
- Parameters:
@@ -68,7 +69,7 @@ description: "Config file reference"
## dipdup.config.AdvancedConfig
-class dipdup.config.AdvancedConfig(**kwargs)
+class dipdup.config.AdvancedConfig(**kwargs)
This section allows users to tune some system-wide options, either experimental or unsuitable for generic configurations.
- Parameters:
@@ -92,7 +93,7 @@ description: "Config file reference"
## dipdup.config.ApiConfig
-class dipdup.config.ApiConfig(**kwargs)
+class dipdup.config.ApiConfig(**kwargs)
Management API config
- Parameters:
@@ -110,7 +111,7 @@ description: "Config file reference"
## dipdup.config.coinbase.CoinbaseDatasourceConfig
-class dipdup.config.coinbase.CoinbaseDatasourceConfig(*args)
+class dipdup.config.coinbase.CoinbaseDatasourceConfig(*args)
Coinbase datasource config
- Parameters:
@@ -130,7 +131,7 @@ description: "Config file reference"
## dipdup.config.evm.EvmContractConfig
-class dipdup.config.evm.EvmContractConfig(*args)
+class dipdup.config.evm.EvmContractConfig(*args)
EVM contract config
- Parameters:
@@ -149,7 +150,7 @@ description: "Config file reference"
## dipdup.config.evm_node.EvmNodeDatasourceConfig
-class dipdup.config.evm_node.EvmNodeDatasourceConfig(*args)
+class dipdup.config.evm_node.EvmNodeDatasourceConfig(*args)
EVM node datasource config
- Parameters:
@@ -169,7 +170,7 @@ description: "Config file reference"
## dipdup.config.evm_etherscan.EvmEtherscanDatasourceConfig
-class dipdup.config.evm_etherscan.EvmEtherscanDatasourceConfig(*args)
+class dipdup.config.evm_etherscan.EvmEtherscanDatasourceConfig(*args)
Etherscan datasource config
- Parameters:
@@ -188,7 +189,7 @@ description: "Config file reference"
## dipdup.config.evm_events.EvmEventsHandlerConfig
-class dipdup.config.evm_events.EvmEventsHandlerConfig(**kwargs)
+class dipdup.config.evm_events.EvmEventsHandlerConfig(**kwargs)
Subsquid event handler
- Parameters:
@@ -207,7 +208,7 @@ description: "Config file reference"
## dipdup.config.evm_events.EvmEventsIndexConfig
-class dipdup.config.evm_events.EvmEventsIndexConfig(*args)
+class dipdup.config.evm_events.EvmEventsIndexConfig(*args)
Subsquid datasource config
- Parameters:
@@ -227,7 +228,7 @@ description: "Config file reference"
## dipdup.config.evm_subsquid.EvmSubsquidDatasourceConfig
-class dipdup.config.evm_subsquid.EvmSubsquidDatasourceConfig(*args)
+class dipdup.config.evm_subsquid.EvmSubsquidDatasourceConfig(*args)
Subsquid datasource config
- Parameters:
@@ -245,7 +246,7 @@ description: "Config file reference"
## dipdup.config.evm_transactions.EvmTransactionsHandlerConfig
-class dipdup.config.evm_transactions.EvmTransactionsHandlerConfig(**kwargs)
+class dipdup.config.evm_transactions.EvmTransactionsHandlerConfig(**kwargs)
Subsquid transaction handler
- Parameters:
@@ -266,7 +267,7 @@ description: "Config file reference"
## dipdup.config.evm_transactions.EvmTransactionsIndexConfig
-class dipdup.config.evm_transactions.EvmTransactionsIndexConfig(*args)
+class dipdup.config.evm_transactions.EvmTransactionsIndexConfig(*args)
Index that uses Subsquid Network as a datasource for transactions
- Parameters:
@@ -286,7 +287,7 @@ description: "Config file reference"
## dipdup.config.HandlerConfig
-class dipdup.config.HandlerConfig(**kwargs)
+class dipdup.config.HandlerConfig(**kwargs)
Base class for index handlers
- Parameters:
@@ -303,7 +304,7 @@ description: "Config file reference"
## dipdup.config.HasuraConfig
-class dipdup.config.HasuraConfig(**kwargs)
+class dipdup.config.HasuraConfig(**kwargs)
Config for the Hasura integration.
- Parameters:
@@ -331,7 +332,7 @@ description: "Config file reference"
## dipdup.config.HookConfig
-class dipdup.config.HookConfig(**kwargs)
+class dipdup.config.HookConfig(**kwargs)
Hook config
- Parameters:
@@ -349,7 +350,7 @@ description: "Config file reference"
## dipdup.config.HttpConfig
-class dipdup.config.HttpConfig(retry_count=None, retry_sleep=None, retry_multiplier=None, ratelimit_rate=None, ratelimit_period=None, ratelimit_sleep=None, connection_limit=None, connection_timeout=None, request_timeout=None, batch_size=None, polling_interval=None, replay=None, replay_path=None, alias=None)
+class dipdup.config.HttpConfig(retry_count=None, retry_sleep=None, retry_multiplier=None, ratelimit_rate=None, ratelimit_period=None, ratelimit_sleep=None, connection_limit=None, connection_timeout=None, request_timeout=None, batch_size=None, polling_interval=None, replay=None, replay_path=None, alias=None)
Advanced configuration of HTTP client
- Parameters:
@@ -378,7 +379,7 @@ description: "Config file reference"
## dipdup.config.http.HttpDatasourceConfig
-class dipdup.config.http.HttpDatasourceConfig(*args)
+class dipdup.config.http.HttpDatasourceConfig(*args)
Generic HTTP datasource config
- Parameters:
@@ -396,7 +397,7 @@ description: "Config file reference"
## dipdup.config.IndexConfig
-class dipdup.config.IndexConfig(*args)
+class dipdup.config.IndexConfig(*args)
Index config
- Parameters:
@@ -413,7 +414,7 @@ description: "Config file reference"
## dipdup.config.DatasourceConfig
-class dipdup.config.DatasourceConfig(*args)
+class dipdup.config.DatasourceConfig(*args)
Base class for datasource configs
- Parameters:
@@ -431,7 +432,7 @@ description: "Config file reference"
## dipdup.config.IndexTemplateConfig
-class dipdup.config.IndexTemplateConfig(*args)
+class dipdup.config.IndexTemplateConfig(*args)
Index template config
- Parameters:
@@ -451,7 +452,7 @@ description: "Config file reference"
## dipdup.config.ipfs.IpfsDatasourceConfig
-class dipdup.config.ipfs.IpfsDatasourceConfig(*args)
+class dipdup.config.ipfs.IpfsDatasourceConfig(*args)
IPFS datasource config
- Parameters:
@@ -469,7 +470,7 @@ description: "Config file reference"
## dipdup.config.JobConfig
-class dipdup.config.JobConfig(*args)
+class dipdup.config.JobConfig(*args)
Job schedule config
- Parameters:
@@ -488,7 +489,7 @@ description: "Config file reference"
## dipdup.config.PostgresDatabaseConfig
-class dipdup.config.PostgresDatabaseConfig(**kwargs)
+class dipdup.config.PostgresDatabaseConfig(**kwargs)
Postgres database connection config
- Parameters:
@@ -513,7 +514,7 @@ description: "Config file reference"
## dipdup.config.PrometheusConfig
-class dipdup.config.PrometheusConfig(**kwargs)
+class dipdup.config.PrometheusConfig(**kwargs)
Config for Prometheus integration.
- Parameters:
@@ -532,7 +533,7 @@ description: "Config file reference"
## dipdup.config.ResolvedHttpConfig
-class dipdup.config.ResolvedHttpConfig(**kwargs)
+class dipdup.config.ResolvedHttpConfig(**kwargs)
Advanced configuration of HTTP client
- Parameters:
@@ -562,7 +563,7 @@ description: "Config file reference"
## dipdup.config.RuntimeConfig
-class dipdup.config.RuntimeConfig(*args)
+class dipdup.config.RuntimeConfig(*args)
Runtime config
- Parameters:
@@ -579,7 +580,7 @@ description: "Config file reference"
## dipdup.config.SentryConfig
-class dipdup.config.SentryConfig(**kwargs)
+class dipdup.config.SentryConfig(**kwargs)
Config for Sentry integration.
- Parameters:
@@ -601,7 +602,7 @@ description: "Config file reference"
## dipdup.config.SqliteDatabaseConfig
-class dipdup.config.SqliteDatabaseConfig(**kwargs)
+class dipdup.config.SqliteDatabaseConfig(**kwargs)
SQLite connection config
- Parameters:
@@ -620,7 +621,7 @@ description: "Config file reference"
## dipdup.config.SystemHookConfig
-class dipdup.config.SystemHookConfig(callback, atomic=False)
+class dipdup.config.SystemHookConfig(callback, atomic=False)
Hook config
- Parameters:
@@ -637,7 +638,7 @@ description: "Config file reference"
## dipdup.config.tezos.TezosContractConfig
-class dipdup.config.tezos.TezosContractConfig(*args)
+class dipdup.config.tezos.TezosContractConfig(*args)
Tezos contract config.
- Parameters:
@@ -656,7 +657,7 @@ description: "Config file reference"
## dipdup.config.tezos.TezosIndexConfig
-class dipdup.config.tezos.TezosIndexConfig(kind, datasources)
+class dipdup.config.tezos.TezosIndexConfig(kind, datasources)
TzKT index config
- Parameters:
@@ -673,7 +674,7 @@ description: "Config file reference"
## dipdup.config.tezos_big_maps.TezosBigMapsHandlerConfig
-class dipdup.config.tezos_big_maps.TezosBigMapsHandlerConfig(callback)
+class dipdup.config.tezos_big_maps.TezosBigMapsHandlerConfig(callback)
Big map handler config
- Parameters:
@@ -691,7 +692,7 @@ description: "Config file reference"
## dipdup.config.tezos_big_maps.TezosBigMapsIndexConfig
-class dipdup.config.tezos_big_maps.TezosBigMapsIndexConfig(kind, datasources)
+class dipdup.config.tezos_big_maps.TezosBigMapsIndexConfig(kind, datasources)
Big map index config
- Parameters:
@@ -712,7 +713,7 @@ description: "Config file reference"
## dipdup.config.tezos_events.TezosEventsHandlerConfig
-class dipdup.config.tezos_events.TezosEventsHandlerConfig(callback)
+class dipdup.config.tezos_events.TezosEventsHandlerConfig(callback)
Event handler config
- Parameters:
@@ -730,7 +731,7 @@ description: "Config file reference"
## dipdup.config.tezos_events.TezosEventsIndexConfig
-class dipdup.config.tezos_events.TezosEventsIndexConfig(kind, datasources)
+class dipdup.config.tezos_events.TezosEventsIndexConfig(kind, datasources)
Event index config
- Parameters:
@@ -750,7 +751,7 @@ description: "Config file reference"
## dipdup.config.tezos_events.TezosEventsUnknownEventHandlerConfig
-class dipdup.config.tezos_events.TezosEventsUnknownEventHandlerConfig(callback)
+class dipdup.config.tezos_events.TezosEventsUnknownEventHandlerConfig(callback)
Unknown event handler config
- Parameters:
@@ -767,7 +768,7 @@ description: "Config file reference"
## dipdup.config.tezos_head.TezosTzktHeadHandlerConfig
-class dipdup.config.tezos_head.TezosTzktHeadHandlerConfig(callback)
+class dipdup.config.tezos_head.TezosTzktHeadHandlerConfig(callback)
Head block handler config
- Parameters:
@@ -783,7 +784,7 @@ description: "Config file reference"
## dipdup.config.tezos_head.TezosHeadIndexConfig
-class dipdup.config.tezos_head.TezosHeadIndexConfig(kind, datasources)
+class dipdup.config.tezos_head.TezosHeadIndexConfig(kind, datasources)
Head block index config
- Parameters:
@@ -801,7 +802,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsHandlerConfig
-class dipdup.config.tezos_operations.TezosOperationsHandlerConfig(callback)
+class dipdup.config.tezos_operations.TezosOperationsHandlerConfig(callback)
Operation handler config
- Parameters:
@@ -818,7 +819,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsHandlerOriginationPatternConfig
-class dipdup.config.tezos_operations.TezosOperationsHandlerOriginationPatternConfig(**kwargs)
+class dipdup.config.tezos_operations.TezosOperationsHandlerOriginationPatternConfig(**kwargs)
Origination handler pattern config
- Parameters:
@@ -840,7 +841,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupCementPatternConfig
-class dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupCementPatternConfig(**kwargs)
+class dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupCementPatternConfig(**kwargs)
Operation handler pattern config
- Parameters:
@@ -861,7 +862,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupExecutePatternConfig
-class dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupExecutePatternConfig(**kwargs)
+class dipdup.config.tezos_operations.TezosOperationsHandlerSmartRollupExecutePatternConfig(**kwargs)
Operation handler pattern config
- Parameters:
@@ -882,7 +883,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsHandlerTransactionPatternConfig
-class dipdup.config.tezos_operations.TezosOperationsHandlerTransactionPatternConfig(**kwargs)
+class dipdup.config.tezos_operations.TezosOperationsHandlerTransactionPatternConfig(**kwargs)
Transaction handler pattern config
- Parameters:
@@ -904,7 +905,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsIndexConfig
-class dipdup.config.tezos_operations.TezosOperationsIndexConfig(kind, datasources)
+class dipdup.config.tezos_operations.TezosOperationsIndexConfig(kind, datasources)
Operation index config
- Parameters:
@@ -926,7 +927,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsPatternConfig
-class dipdup.config.tezos_operations.TezosOperationsPatternConfig(**kwargs)
+class dipdup.config.tezos_operations.TezosOperationsPatternConfig(**kwargs)
Base class for pattern config items.
Contains methods for import and method signature generation during handler callbacks codegen.
@@ -943,7 +944,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsUnfilteredHandlerConfig
-class dipdup.config.tezos_operations.TezosOperationsUnfilteredHandlerConfig(callback)
+class dipdup.config.tezos_operations.TezosOperationsUnfilteredHandlerConfig(callback)
Handler of unfiltered operation index
- Parameters:
@@ -959,7 +960,7 @@ description: "Config file reference"
## dipdup.config.tezos_operations.TezosOperationsUnfilteredIndexConfig
-class dipdup.config.tezos_operations.TezosOperationsUnfilteredIndexConfig(kind, datasources)
+class dipdup.config.tezos_operations.TezosOperationsUnfilteredIndexConfig(kind, datasources)
Operation index config
- Parameters:
@@ -980,7 +981,7 @@ description: "Config file reference"
## dipdup.config.tezos_tzkt.TezosTzktDatasourceConfig
-class dipdup.config.tezos_tzkt.TezosTzktDatasourceConfig(*args)
+class dipdup.config.tezos_tzkt.TezosTzktDatasourceConfig(*args)
TzKT datasource config
- Parameters:
@@ -1001,7 +1002,7 @@ description: "Config file reference"
## dipdup.config.tezos_token_balances.TezosTokenBalancesHandlerConfig
-class dipdup.config.tezos_token_balances.TezosTokenBalancesHandlerConfig(callback)
+class dipdup.config.tezos_token_balances.TezosTokenBalancesHandlerConfig(callback)
Token balance handler config
- Parameters:
@@ -1019,7 +1020,7 @@ description: "Config file reference"
## dipdup.config.tezos_token_balances.TezosTokenBalancesIndexConfig
-class dipdup.config.tezos_token_balances.TezosTokenBalancesIndexConfig(kind, datasources)
+class dipdup.config.tezos_token_balances.TezosTokenBalancesIndexConfig(kind, datasources)
Token balance index config
- Parameters:
@@ -1039,7 +1040,7 @@ description: "Config file reference"
## dipdup.config.tezos_token_transfers.TezosTokenTransfersHandlerConfig
-class dipdup.config.tezos_token_transfers.TezosTokenTransfersHandlerConfig(callback)
+class dipdup.config.tezos_token_transfers.TezosTokenTransfersHandlerConfig(callback)
Token transfer handler config
- Parameters:
@@ -1059,7 +1060,7 @@ description: "Config file reference"
## dipdup.config.tezos_token_transfers.TezosTokenTransfersIndexConfig
-class dipdup.config.tezos_token_transfers.TezosTokenTransfersIndexConfig(kind, datasources)
+class dipdup.config.tezos_token_transfers.TezosTokenTransfersIndexConfig(kind, datasources)
Token transfer index config
- Parameters:
@@ -1079,7 +1080,7 @@ description: "Config file reference"
## dipdup.config.starknet.StarknetContractConfig
-class dipdup.config.starknet.StarknetContractConfig(*args)
+class dipdup.config.starknet.StarknetContractConfig(*args)
Starknet contract config
- Parameters:
@@ -1098,7 +1099,7 @@ description: "Config file reference"
## dipdup.config.starknet_events.StarknetEventsHandlerConfig
-class dipdup.config.starknet_events.StarknetEventsHandlerConfig(callback)
+class dipdup.config.starknet_events.StarknetEventsHandlerConfig(callback)
Subsquid event handler
- Parameters:
@@ -1116,7 +1117,7 @@ description: "Config file reference"
## dipdup.config.starknet_events.StarknetEventsIndexConfig
-class dipdup.config.starknet_events.StarknetEventsIndexConfig(kind, datasources)
+class dipdup.config.starknet_events.StarknetEventsIndexConfig(kind, datasources)
Starknet events index config
- Parameters:
@@ -1136,7 +1137,7 @@ description: "Config file reference"
## dipdup.config.starknet_node.StarknetNodeDatasourceConfig
-class dipdup.config.starknet_node.StarknetNodeDatasourceConfig(*args)
+class dipdup.config.starknet_node.StarknetNodeDatasourceConfig(*args)
Starknet node datasource config
- Parameters:
@@ -1156,7 +1157,7 @@ description: "Config file reference"
## dipdup.config.starknet_subsquid.StarknetSubsquidDatasourceConfig
-class dipdup.config.starknet_subsquid.StarknetSubsquidDatasourceConfig(*args)
+class dipdup.config.starknet_subsquid.StarknetSubsquidDatasourceConfig(*args)
Subsquid datasource config
- Parameters:
@@ -1174,7 +1175,7 @@ description: "Config file reference"
## dipdup.config.substrate.SubstrateRuntimeConfig
-class dipdup.config.substrate.SubstrateRuntimeConfig(*args)
+class dipdup.config.substrate.SubstrateRuntimeConfig(*args)
Substrate runtime config
- Parameters:
@@ -1191,7 +1192,7 @@ description: "Config file reference"
## dipdup.config.substrate_events.SubstrateEventsHandlerConfig
-class dipdup.config.substrate_events.SubstrateEventsHandlerConfig(callback)
+class dipdup.config.substrate_events.SubstrateEventsHandlerConfig(callback)
Subsquid event handler
- Parameters:
@@ -1208,7 +1209,7 @@ description: "Config file reference"
## dipdup.config.substrate_events.SubstrateEventsIndexConfig
-class dipdup.config.substrate_events.SubstrateEventsIndexConfig(kind, datasources)
+class dipdup.config.substrate_events.SubstrateEventsIndexConfig(kind, datasources)
Subsquid datasource config
- Parameters:
@@ -1230,7 +1231,7 @@ description: "Config file reference"
## dipdup.config.substrate_subsquid.SubstrateSubsquidDatasourceConfig
-class dipdup.config.substrate_subsquid.SubstrateSubsquidDatasourceConfig(*args)
+class dipdup.config.substrate_subsquid.SubstrateSubsquidDatasourceConfig(*args)
Subsquid datasource config
- Parameters:
@@ -1248,7 +1249,7 @@ description: "Config file reference"
## dipdup.config.substrate_subscan.SubstrateSubscanDatasourceConfig
-class dipdup.config.substrate_subscan.SubstrateSubscanDatasourceConfig(*args)
+class dipdup.config.substrate_subscan.SubstrateSubscanDatasourceConfig(*args)
Subscan datasource config
- Parameters:
@@ -1267,7 +1268,7 @@ description: "Config file reference"
## dipdup.config.tzip_metadata.TzipMetadataDatasourceConfig
-class dipdup.config.tzip_metadata.TzipMetadataDatasourceConfig(*args)
+class dipdup.config.tzip_metadata.TzipMetadataDatasourceConfig(*args)
DipDup Metadata datasource config
- Parameters:
diff --git a/docs/7.references/3.context.md b/docs/7.references/3.context.md
index eba8986bb..2f286b646 100644
--- a/docs/7.references/3.context.md
+++ b/docs/7.references/3.context.md
@@ -10,7 +10,7 @@ description: "Context reference"
## dipdup.context.DipDupContext
-class dipdup.context.DipDupContext(config, package, datasources, transactions)
+class dipdup.context.DipDupContext(config, package, datasources, transactions)
Common execution context for handler and hook callbacks.
- Parameters:
@@ -29,7 +29,7 @@ description: "Context reference"
## dipdup.context.HandlerContext
-class dipdup.context.HandlerContext(config, package, datasources, transactions, logger, handler_config)
+class dipdup.context.HandlerContext(config, package, datasources, transactions, logger, handler_config)
Execution context of handler callbacks.
- Parameters:
@@ -49,7 +49,7 @@ description: "Context reference"
## dipdup.context.HookContext
-class dipdup.context.HookContext(config, package, datasources, transactions, logger, hook_config)
+class dipdup.context.HookContext(config, package, datasources, transactions, logger, hook_config)
Execution context of hook callbacks.
- Parameters:
@@ -69,7 +69,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.add_contract
-async DipDupContext.add_contract(kind, name, address=None, typename=None, code_hash=None)
+async DipDupContext.add_contract(kind, name, address=None, typename=None, code_hash=None)
Adds contract to the inventory.
- Parameters:
@@ -91,7 +91,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.add_index
-async DipDupContext.add_index(name, template, values, first_level=0, last_level=0, state=None)
+async DipDupContext.add_index(name, template, values, first_level=0, last_level=0, state=None)
Adds a new index from template.
- Parameters:
@@ -114,7 +114,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.execute_sql
-async DipDupContext.execute_sql(name, *args, **kwargs)
+async DipDupContext.execute_sql(name, *args, **kwargs)
Executes SQL script(s) with given name.
If the name path is a directory, all .sql scripts within it will be executed in alphabetical order.
@@ -135,7 +135,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.execute_sql_query
-async DipDupContext.execute_sql_query(name, *args)
+async DipDupContext.execute_sql_query(name, *args)
Executes SQL query with given name included with the project
- Parameters:
@@ -155,7 +155,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.fire_hook
-async DipDupContext.fire_hook(name, wait=True, **kwargs)
+async DipDupContext.fire_hook(name, wait=True, **kwargs)
Fire hook with given name and arguments.
- Parameters:
@@ -303,7 +303,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.reindex
-async DipDupContext.reindex(reason=None, **context)
+async DipDupContext.reindex(reason=None, **context)
Drops the entire database and starts the indexing process from scratch.
- Parameters:
@@ -322,7 +322,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.restart
-async DipDupContext.restart()
+async DipDupContext.restart()
Restart process and continue indexing.
- Return type:
@@ -335,7 +335,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.rollback
-async DipDupContext.rollback(index, from_level, to_level)
+async DipDupContext.rollback(index, from_level, to_level)
Rollback index to a given level reverting all changes made since that level.
- Parameters:
@@ -355,7 +355,7 @@ description: "Context reference"
## dipdup.context.DipDupContext.update_contract_metadata
-async DipDupContext.update_contract_metadata(network, address, metadata)
+async DipDupContext.update_contract_metadata(network, address, metadata)
Inserts or updates corresponding rows in the internal dipdup_contract_metadata table
to provide a generic metadata interface (see docs).
@@ -376,7 +376,7 @@ to provide a generic metadata interface (see docs).
## dipdup.context.DipDupContext.update_token_metadata
-async DipDupContext.update_token_metadata(network, address, token_id, metadata)
+async DipDupContext.update_token_metadata(network, address, token_id, metadata)
Inserts or updates corresponding rows in the internal dipdup_token_metadata table
to provide a generic metadata interface (see docs).
diff --git a/docs/7.references/4.models.md b/docs/7.references/4.models.md
index 8a2e1e545..ea70d49e1 100644
--- a/docs/7.references/4.models.md
+++ b/docs/7.references/4.models.md
@@ -14,7 +14,7 @@ description: "Models reference"
## dipdup.models.CachedModel
-class dipdup.models.CachedModel(**kwargs)
+class dipdup.models.CachedModel(**kwargs)
- Parameters:
kwargs (Any)
@@ -26,7 +26,7 @@ description: "Models reference"
## dipdup.models.Model
-class dipdup.models.Model(**kwargs)
+class dipdup.models.Model(**kwargs)
Base class for DipDup project models
- Parameters:
@@ -44,7 +44,7 @@ description: "Models reference"
## dipdup.models.ContractMetadata
-class dipdup.models.ContractMetadata(**kwargs)
+class dipdup.models.ContractMetadata(**kwargs)
- Parameters:
kwargs (Any)
@@ -56,7 +56,7 @@ description: "Models reference"
## dipdup.models.Head
-class dipdup.models.Head(**kwargs)
+class dipdup.models.Head(**kwargs)
- Parameters:
kwargs (Any)
@@ -68,7 +68,7 @@ description: "Models reference"
## dipdup.models.TokenMetadata
-class dipdup.models.TokenMetadata(**kwargs)
+class dipdup.models.TokenMetadata(**kwargs)
- Parameters:
kwargs (Any)
@@ -80,7 +80,7 @@ description: "Models reference"
## dipdup.models.Contract
-class dipdup.models.Contract(**kwargs)
+class dipdup.models.Contract(**kwargs)
- Parameters:
kwargs (Any)
@@ -92,7 +92,7 @@ description: "Models reference"
## dipdup.models.ContractKind
-class dipdup.models.ContractKind(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.ContractKind(*values)
Mapping for contract kind in
@@ -100,7 +100,7 @@ description: "Models reference"
## dipdup.models.Index
-class dipdup.models.Index(**kwargs)
+class dipdup.models.Index(**kwargs)
- Parameters:
kwargs (Any)
@@ -112,14 +112,14 @@ description: "Models reference"
## dipdup.models.IndexStatus
-class dipdup.models.IndexStatus(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.IndexStatus(*values)
## dipdup.models.IndexType
-class dipdup.models.IndexType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.IndexType(*values)
Enum for dipdup.models.Index
@@ -127,7 +127,7 @@ description: "Models reference"
## dipdup.models.Schema
-class dipdup.models.Schema(**kwargs)
+class dipdup.models.Schema(**kwargs)
- Parameters:
kwargs (Any)
@@ -139,7 +139,7 @@ description: "Models reference"
## dipdup.models.ReindexingAction
-class dipdup.models.ReindexingAction(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.ReindexingAction(*values)
Action that should be performed on reindexing
- Parameters:
@@ -156,7 +156,7 @@ description: "Models reference"
## dipdup.models.ReindexingReason
-class dipdup.models.ReindexingReason(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.ReindexingReason(*values)
Reason that caused reindexing
- Parameters:
@@ -175,7 +175,7 @@ description: "Models reference"
## dipdup.models.SkipHistory
-class dipdup.models.SkipHistory(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.SkipHistory(*values)
Whether to skip indexing big map history and use only current state
- Parameters:
@@ -192,7 +192,7 @@ description: "Models reference"
## dipdup.models.Meta
-class dipdup.models.Meta(**kwargs)
+class dipdup.models.Meta(**kwargs)
- Parameters:
kwargs (Any)
@@ -204,7 +204,7 @@ description: "Models reference"
## dipdup.models.ModelUpdate
-class dipdup.models.ModelUpdate(**kwargs)
+class dipdup.models.ModelUpdate(**kwargs)
Model update created within versioned transactions
- Parameters:
@@ -217,7 +217,7 @@ description: "Models reference"
## dipdup.models.ModelUpdateAction
-class dipdup.models.ModelUpdateAction(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.ModelUpdateAction(*values)
Mapping for actions in model update
@@ -234,7 +234,7 @@ description: "Models reference"
## dipdup.models.evm.EvmTransaction
-class dipdup.models.evm.EvmTransaction(data: dipdup.models.evm.EvmTransactionData, input: InputT)
+class dipdup.models.evm.EvmTransaction(data: dipdup.models.evm.EvmTransactionData, input: InputT)
- Parameters:
@@ -249,7 +249,7 @@ description: "Models reference"
## dipdup.models.evm.EvmTransactionData
-class dipdup.models.evm.EvmTransactionData(access_list: tuple[dict[str, dipdup.fields.Any], ...] | None, block_hash: str, chain_id: int | None, contract_address: str | None, cumulative_gas_used: int | None, effective_gas_price: int | None, from_: str, gas: int, gas_price: int, gas_used: int | None, hash: str, input: str, level: int, max_fee_per_gas: int | None, max_priority_fee_per_gas: int | None, nonce: int, r: str | None, s: str | None, status: int | None, timestamp: int, to: str | None, transaction_index: int | None, type: int | None, value: int | None, v: int | None, y_parity: bool | None)
+class dipdup.models.evm.EvmTransactionData(access_list: tuple[dict[str, dipdup.fields.Any], ...] | None, block_hash: str, chain_id: int | None, contract_address: str | None, cumulative_gas_used: int | None, effective_gas_price: int | None, from_: str, gas: int, gas_price: int, gas_used: int | None, hash: str, input: str, level: int, max_fee_per_gas: int | None, max_priority_fee_per_gas: int | None, nonce: int, r: str | None, s: str | None, status: int | None, timestamp: int, to: str | None, transaction_index: int | None, type: int | None, value: int | None, v: int | None, y_parity: bool | None)
- Parameters:
@@ -288,7 +288,7 @@ description: "Models reference"
## dipdup.models.evm.EvmEvent
-class dipdup.models.evm.EvmEvent(data: dipdup.models.evm.EvmEventData, payload: PayloadT)
+class dipdup.models.evm.EvmEvent(data: dipdup.models.evm.EvmEventData, payload: PayloadT)
- Parameters:
@@ -303,7 +303,7 @@ description: "Models reference"
## dipdup.models.evm.EvmEventData
-class dipdup.models.evm.EvmEventData(address: str, block_hash: str, data: str, level: int, log_index: int, removed: bool, timestamp: int, topics: tuple[str, ...], transaction_hash: str, transaction_index: int)
+class dipdup.models.evm.EvmEventData(address: str, block_hash: str, data: str, level: int, log_index: int, removed: bool, timestamp: int, topics: tuple[str, ...], transaction_hash: str, transaction_index: int)
- Parameters:
@@ -331,7 +331,7 @@ description: "Models reference"
## dipdup.models.starknet.StarknetEvent
-class dipdup.models.starknet.StarknetEvent(data: dipdup.models.starknet.StarknetEventData, payload: PayloadT)
+class dipdup.models.starknet.StarknetEvent(data: dipdup.models.starknet.StarknetEventData, payload: PayloadT)
- Parameters:
@@ -346,7 +346,7 @@ description: "Models reference"
## dipdup.models.starknet.StarknetEventData
-class dipdup.models.starknet.StarknetEventData(level: int, block_hash: str, transaction_index: int | None, transaction_hash: str, timestamp: int | None, from_address: str, keys: tuple[str, ...], data: tuple[str, ...])
+class dipdup.models.starknet.StarknetEventData(level: int, block_hash: str, transaction_index: int | None, transaction_hash: str, timestamp: int | None, from_address: str, keys: tuple[str, ...], data: tuple[str, ...])
- Parameters:
@@ -367,7 +367,7 @@ description: "Models reference"
## dipdup.models.starknet.StarknetTransactionData
-class dipdup.models.starknet.StarknetTransactionData(level: int, block_hash: str, transaction_index: int, transaction_hash: str, timestamp: int, contract_address: str | None, entry_point_selector: str | None, calldata: tuple[str, ...] | None, max_fee: str | None, version: str, signature: tuple[str, ...] | None, nonce: str | None, type: str, sender_address: str | None, class_hash: str | None, compiled_class_hash: str | None, contract_address_salt: str | None, constructor_calldata: tuple[str, ...] | None)
+class dipdup.models.starknet.StarknetTransactionData(level: int, block_hash: str, transaction_index: int, transaction_hash: str, timestamp: int, contract_address: str | None, entry_point_selector: str | None, calldata: tuple[str, ...] | None, max_fee: str | None, version: str, signature: tuple[str, ...] | None, nonce: str | None, type: str, sender_address: str | None, class_hash: str | None, compiled_class_hash: str | None, contract_address_salt: str | None, constructor_calldata: tuple[str, ...] | None)
- Parameters:
@@ -403,7 +403,7 @@ description: "Models reference"
## dipdup.models.substrate.SubstrateEvent
-class dipdup.models.substrate.SubstrateEvent(data: dipdup.models.substrate.SubstrateEventData, runtime: dipdup.runtimes.SubstrateRuntime)
+class dipdup.models.substrate.SubstrateEvent(data: dipdup.models.substrate.SubstrateEventData, runtime: dipdup.runtimes.SubstrateRuntime)
- Parameters:
@@ -418,7 +418,7 @@ description: "Models reference"
## dipdup.models.substrate.SubstrateEventData
-class dipdup.models.substrate.SubstrateEventData(*, name: str, index: int, extrinsic_index: int, call_address: list[str] | None, args: list[dipdup.fields.Any] | None = None, decoded_args: dict[str, dipdup.fields.Any] | list[dipdup.fields.Any] | None = None, header: dipdup.models.substrate._BlockHeader, header_extra: dipdup.models.substrate._BlockHeaderExtra | None)
+class dipdup.models.substrate.SubstrateEventData(*, name: str, index: int, extrinsic_index: int, call_address: list[str] | None, args: list[dipdup.fields.Any] | None = None, decoded_args: dict[str, dipdup.fields.Any] | list[dipdup.fields.Any] | None = None, header: dipdup.models.substrate._BlockHeader, header_extra: dipdup.models.substrate._BlockHeaderExtra | None)
- Parameters:
@@ -439,7 +439,7 @@ description: "Models reference"
## dipdup.models.substrate.SubstrateHeadBlockData
-class dipdup.models.substrate.SubstrateHeadBlockData
+class dipdup.models.substrate.SubstrateHeadBlockData
@@ -451,7 +451,7 @@ description: "Models reference"
## dipdup.models.tezos.TezosBigMapAction
-class dipdup.models.tezos.TezosBigMapAction(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)
+class dipdup.models.tezos.TezosBigMapAction(*values)
Mapping for action in TzKT response
@@ -459,7 +459,7 @@ description: "Models reference"
## dipdup.models.tezos.TezosBigMapData
-class dipdup.models.tezos.TezosBigMapData(id, level, operation_id, timestamp, bigmap, contract_address, path, action, active, key=None, value=None)
+class dipdup.models.tezos.TezosBigMapData(id, level, operation_id, timestamp, bigmap, contract_address, path, action, active, key=None, value=None)
Basic structure for big map diffs from TzKT response
- Parameters:
@@ -484,7 +484,7 @@ description: "Models reference"
## dipdup.models.tezos.TezosBigMapDiff
-class dipdup.models.tezos.TezosBigMapDiff(action, data, key, value)
+class dipdup.models.tezos.TezosBigMapDiff(action, data, key, value)
Wrapper for matched big map diff with typed data passed to the handler
- Parameters:
@@ -502,7 +502,7 @@ description: "Models reference"
## dipdup.models.tezos.TezosBlockData
-class dipdup.models.tezos.TezosBlockData(level, hash, timestamp, proto, validations, deposit, reward, fees, nonce_revealed, priority=None, baker_address=None, baker_alias=None)
+class dipdup.models.tezos.TezosBlockData(level, hash, timestamp, proto, validations, deposit, reward, fees, nonce_revealed, priority=None, baker_address=None, baker_alias=None)
Basic structure for blocks received from TzKT REST API
- Parameters:
@@ -528,7 +528,7 @@ description: "Models reference"
## dipdup.models.tezos.TezosEvent
-class dipdup.models.tezos.TezosEvent(data, payload)
+class dipdup.models.tezos.TezosEvent(data, payload)
- Parameters:
-
@@ -57,7 +57,7 @@ description: "Context reference"
config (DipDupConfig) – DipDup configuration
package (DipDupPackage) – DipDup package
datasources (dict[str, Datasource[Any]]) – Mapping of available datasources
-transactions (TransactionManager) – Transaction manager (don’t use it directly)
+transactions (TransactionManager) – Transaction manager (low-level interface)
logger (Logger) – Context-aware logger instance
hook_config (HookConfig) – Configuration of the current hook
diff --git a/src/dipdup/mcp.py b/src/dipdup/mcp.py
index 50370b1b8..738baee28 100644
--- a/src/dipdup/mcp.py
+++ b/src/dipdup/mcp.py
@@ -194,11 +194,14 @@ def wrapper(func: Any) -> Any:
msg = f'Tool `{name}` is already registered'
raise FrameworkException(msg)
+ from mcp.server.fastmcp.tools.base import Tool
+
+ tool_info = Tool.from_function(func, name=name, description=description)
+
_user_tools[name] = types.Tool(
name=name,
description=description,
- # FIXME: Generate schema from signature
- inputSchema={'type': 'object'},
+ inputSchema=tool_info.parameters,
)
_user_tools_fn[name] = func
From c5424639436788b0ea86d038ec7fad5e9816b8be Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Fri, 21 Mar 2025 15:12:52 -0300
Subject: [PATCH 27/30] docs
---
docs/5.advanced/7.mcp.md | 41 +++++++++++++++++++++++++++++++++++-----
src/dipdup/mcp.py | 1 +
2 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/docs/5.advanced/7.mcp.md b/docs/5.advanced/7.mcp.md
index 4c5bf43bb..cc5bd401e 100644
--- a/docs/5.advanced/7.mcp.md
+++ b/docs/5.advanced/7.mcp.md
@@ -5,17 +5,27 @@ description: "MCP is an open protocol that standardizes how applications provide
# MCP integration
+::banner{type="warning"}
+**You are early!**
+
+MCP is a pretty recent technology, so many tools might not work as expected. Consult the [Known issues](#known-issues) section for more information and open the ticket in [GitHub issues](https://github.com/dipdup-io/dipdup/issues) if you encounter any problems.
+::
+
## Introduction
The **Model Context Protocol** (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.
-**DipDup** provides an MCP server with several built-in resources to help LLMs understand the context of your project and the current state of the indexer.
+There are three types of MCP primitives (citing from the docs):
-You can also implement your own tools and resources specific to your project.
+- **Resources** are **application-controlled** and allow to expose data and content that can be read by clients and used as context for LLM interactions.
+- **Tools** are **model-controlled** and enable servers to expose executable functionality to clients, such as interacting with external systems, performing computations, and taking actions in the real world.
+- **Prompts** are **user-controlled** enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs
+
+**DipDup** provides an MCP server with several built-in primitives to help LLMs understand the context of your project and the current state of the indexer. You can also implement your own tools and resources specific to your project.
## Running MCP server
-DipDup MCP server runs in a separate process. To start it, run the following command:
+DipDup MCP server runs in a separate process via the SSE transport. To start it, run the following command:
```shell
dipdup mcp run
@@ -106,9 +116,22 @@ async def tool():
ctx.logger.info('Hello from MCP tool!')
```
+### Calling other callbacks
+
+You can use the `dipdup.mcp` functions to read other resources or call tools from your callbacks.
+
+```python
+from dipdup import mcp
+
+async def tool():
+ result = await mcp.call_tool('my_tool', {})
+```
+
+For a low-level access you can use `dipdup.mcp.server` singleton to interact with the running server.
+
### Interacting with running indexer
-TBD
+TBD
### Debugging
@@ -123,9 +146,17 @@ logging:
'': DEBUG
```
+## Known issues
+
+- **DipDup** doesn't support custom prompts at the moment.
+- **Cursor** fails to discover resources exposed by DipDup server. Tools work fine.
+- **VSCode** community extensions for MCP don't work out-of-the-box.
+- **Claude Desktop** doesn't support SSE-based MCP servers; gateway tools are required.
+
## Further reading
- [For Server Developers - Model Context Protocol](https://modelcontextprotocol.io/quickstart/server)
- [Example Servers - Model Context Protocol](https://modelcontextprotocol.io/examples)
-- [qpd-v/mcp-guide](https://github.com/qpd-v/mcp-guide) - a tutorial server that helps users understand MCP concepts, provides interactive examples, and demonstrates best practices for building MCP integrations. Also, an awesome-list of related projects.
+- [qpd-v/mcp-guide](https://github.com/qpd-v/mcp-guide) - a tutorial server that helps users understand MCP concepts, provides interactive examples, and demonstrates best practices for building MCP integrations. Contains [awesome-list](https://raw.githubusercontent.com/qpd-v/mcp-guide/refs/heads/main/src/servers/serverlist.txt) of related projects.
- [anjor/coinmarket-mcp-server](https://github.com/anjor/coinmarket-mcp-server) - example MCP server for CoinMarketCap API.
+- [ample-education/cursor-resources](https://github.com/ample-education/cursor-resources) - a collection of resources to maximize productivity with Cursor: prompts, rules, MCP servers etc.
diff --git a/src/dipdup/mcp.py b/src/dipdup/mcp.py
index 738baee28..137f670d5 100644
--- a/src/dipdup/mcp.py
+++ b/src/dipdup/mcp.py
@@ -123,6 +123,7 @@ def _set_ctx(ctx: McpContext) -> None:
_ctx = ctx
+# TODO: Add instructions
server: mcp.server.Server[Any] = mcp.server.Server(name='DipDup')
_user_tools: dict[str, types.Tool] = {}
_user_tools_fn: dict[str, Callable[..., Awaitable[Iterable[str]]]] = {}
From 9ba9e410e59ef0b2efefaab2346b5ce003e8c5ee Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Fri, 21 Mar 2025 15:35:00 -0300
Subject: [PATCH 28/30] docs and package section
---
docs/5.advanced/7.mcp.md | 69 +++++++++++++++++++-----
src/demo_blank/mcp/.keep | 0
src/demo_evm_events/mcp/.keep | 0
src/demo_evm_transactions/mcp/.keep | 0
src/demo_evm_uniswap/mcp/.keep | 0
src/demo_starknet_events/mcp/.keep | 0
src/demo_substrate_events/mcp/.keep | 0
src/demo_tezos_auction/mcp/.keep | 0
src/demo_tezos_dao/mcp/.keep | 0
src/demo_tezos_dex/mcp/.keep | 0
src/demo_tezos_domains/mcp/.keep | 0
src/demo_tezos_events/mcp/.keep | 0
src/demo_tezos_factories/mcp/.keep | 0
src/demo_tezos_head/mcp/.keep | 0
src/demo_tezos_nft_marketplace/mcp/.keep | 0
src/demo_tezos_raw/mcp/.keep | 0
src/demo_tezos_token/mcp/.keep | 0
src/demo_tezos_token_balances/mcp/.keep | 0
src/demo_tezos_token_transfers/mcp/.keep | 0
src/dipdup/package.py | 3 ++
20 files changed, 58 insertions(+), 14 deletions(-)
create mode 100644 src/demo_blank/mcp/.keep
create mode 100644 src/demo_evm_events/mcp/.keep
create mode 100644 src/demo_evm_transactions/mcp/.keep
create mode 100644 src/demo_evm_uniswap/mcp/.keep
create mode 100644 src/demo_starknet_events/mcp/.keep
create mode 100644 src/demo_substrate_events/mcp/.keep
create mode 100644 src/demo_tezos_auction/mcp/.keep
create mode 100644 src/demo_tezos_dao/mcp/.keep
create mode 100644 src/demo_tezos_dex/mcp/.keep
create mode 100644 src/demo_tezos_domains/mcp/.keep
create mode 100644 src/demo_tezos_events/mcp/.keep
create mode 100644 src/demo_tezos_factories/mcp/.keep
create mode 100644 src/demo_tezos_head/mcp/.keep
create mode 100644 src/demo_tezos_nft_marketplace/mcp/.keep
create mode 100644 src/demo_tezos_raw/mcp/.keep
create mode 100644 src/demo_tezos_token/mcp/.keep
create mode 100644 src/demo_tezos_token_balances/mcp/.keep
create mode 100644 src/demo_tezos_token_transfers/mcp/.keep
diff --git a/docs/5.advanced/7.mcp.md b/docs/5.advanced/7.mcp.md
index cc5bd401e..b9c6360bd 100644
--- a/docs/5.advanced/7.mcp.md
+++ b/docs/5.advanced/7.mcp.md
@@ -84,9 +84,11 @@ There is also [lightconetech/mcp-gateway](https://github.com/lightconetech/mcp-g
To make your server more useful, you can implement custom MCP primitives specific to your project. For example, let's implement a tool that provides some information about token holders.
-Create a new file `mcp.py` in the `handlers` directory:
+If you used DipDup prior to 8.3 release, run `dipdup init` command to update the project structure.
-```python [handlers/mcp.py]
+Create a new file `tools.py` in the `mcp` project directory. In this example, we define a new tool `TopHolders` that returns the top 10 token holders sorted by their balance.
+
+```python [mcp/tools.py]
from dipdup import mcp
from demo_evm_events import models
@@ -94,14 +96,12 @@ from demo_evm_events import models
@mcp.tool('TopHolders', 'Get top token holders')
async def tool_holders() -> str:
holders = await models.Holder.filter().order_by('-balance').limit(10).all()
- res = 'Top holders:\n'
+ res = 'Top holders:\n\n'
for holder in holders:
- res += f'{holder.address}: {holder.balance}\n'
+ res += f'- {holder.address}: {holder.balance}\n'
return res
```
-In this example, we define a new tool `TopHolders` that returns the top 10 token holders sorted by their balance.
-
### Using application context
You can use the application context the same way as in handlers and hooks. Use the `mcp.get_ctx()` function to get the context object.
@@ -110,10 +110,11 @@ You can use the application context the same way as in handlers and hooks. Use t
from dipdup import mcp
from dipdup.context import McpContext
+@mcp.tool(...)
async def tool():
- ctx: McpContext = mcp.get_ctx()
+ ctx: McpContext = mcp.get_ctx()
- ctx.logger.info('Hello from MCP tool!')
+ ctx.logger.info('Hello from MCP tool!')
```
### Calling other callbacks
@@ -123,23 +124,63 @@ You can use the `dipdup.mcp` functions to read other resources or call tools fro
```python
from dipdup import mcp
+@mcp.tool(...)
async def tool():
- result = await mcp.call_tool('my_tool', {})
+ result = await mcp.call_tool('my_tool', {})
```
For a low-level access you can use `dipdup.mcp.server` singleton to interact with the running server.
### Interacting with running indexer
-TBD
+DipDup provides [management API](../7.references/4.api.md) to interact with the running indexer. For example you can use it to add indexes in runtime. First, add running indexer as a HTTP datasource:
+
+```yaml [dipdup.yaml]
+datasources:
+ indexer:
+ kind: http
+ # NOTE: Default for Compose stack
+ url: http://api:46339
+```
+
+Then, call this datasource from your MCP tool:
+
+```python
+from dipdup import mcp
+
+@mcp.tool(...)
+async def tool():
+ ctx = mcp.get_ctx()
+ datasource = ctx.get_http_datasource('indexer')
+ response = await datasource.post(
+ '/add_index',
+ params={
+ 'name': 'my_index',
+ 'template': 'my_template',
+ 'values': {'param': 'value'},
+ 'first_level': 0,
+ 'last_level': 1000,
+ }
+ )
+```
+
+## Running in Docker
+
+You can find a Compose file in `deploy/` project directory that runs both the indexer and the MCP server in Docker containers. To use this manifest, use the following command:
+
+```shell [Terminal]
+make up COMPOSE=deploy/compose.mcp.yaml
+```
-### Debugging
+### Debugging and troubleshooting
-To check if your tool is working correctly before using it in the client, you can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) app.
+To check if your tool is working correctly before using it in the client, you can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) app:
-Run `npx @modelcontextprotocol/inspector`, open `http://127.0.0.1:5173/` in your browser, choose SSE transport and connect to `http://127.0.0.1:9999/sse`.
+1. Run `npx @modelcontextprotocol/inspector`
+2. Open `http://127.0.0.1:5173/` in your browser
+3. Choose SSE transport and connect to `http://127.0.0.1:9999/sse`.
-If you suspect there's a bug in the framework, you can enable full logging to get some insights into the MCP server:
+You can also enable full logging to get some insights into the MCP server:
```yaml [dipdup.yaml]
logging:
diff --git a/src/demo_blank/mcp/.keep b/src/demo_blank/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_evm_events/mcp/.keep b/src/demo_evm_events/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_evm_transactions/mcp/.keep b/src/demo_evm_transactions/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_evm_uniswap/mcp/.keep b/src/demo_evm_uniswap/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_starknet_events/mcp/.keep b/src/demo_starknet_events/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_substrate_events/mcp/.keep b/src/demo_substrate_events/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_auction/mcp/.keep b/src/demo_tezos_auction/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_dao/mcp/.keep b/src/demo_tezos_dao/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_dex/mcp/.keep b/src/demo_tezos_dex/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_domains/mcp/.keep b/src/demo_tezos_domains/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_events/mcp/.keep b/src/demo_tezos_events/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_factories/mcp/.keep b/src/demo_tezos_factories/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_head/mcp/.keep b/src/demo_tezos_head/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_nft_marketplace/mcp/.keep b/src/demo_tezos_nft_marketplace/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_raw/mcp/.keep b/src/demo_tezos_raw/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_token/mcp/.keep b/src/demo_tezos_token/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_token_balances/mcp/.keep b/src/demo_tezos_token_balances/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/demo_tezos_token_transfers/mcp/.keep b/src/demo_tezos_token_transfers/mcp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/dipdup/package.py b/src/dipdup/package.py
index 8e2b400ee..a77e84e02 100644
--- a/src/dipdup/package.py
+++ b/src/dipdup/package.py
@@ -77,6 +77,7 @@ def __init__(self, root: Path, quiet: bool = False) -> None:
self.models = root / 'models'
self.sql = root / 'sql'
self.types = root / 'types'
+ self.mcp = root / 'mcp'
# NOTE: Optional, created if aerich is installed
self.migrations = root / 'migrations'
@@ -128,6 +129,7 @@ def skel(self) -> dict[Path, str | None]:
self.models: '**/*.py',
self.sql: '**/*.sql',
self.types: '**/*.py',
+ self.mcp: '**/*.py',
# NOTE: Python metadata
Path(PEP_561_MARKER): None,
}
@@ -197,6 +199,7 @@ def verify(self) -> None:
import_submodules(f'{self.name}.handlers')
import_submodules(f'{self.name}.hooks')
import_submodules(f'{self.name}.types')
+ import_submodules(f'{self.name}.mcp')
def get_type(self, typename: str, module: str, name: str) -> type[BaseModel]:
key = f'{typename}{module}{name}'
From b6e27da99c9c0b9c6a00a0ea834c1004a8e2923a Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Fri, 21 Mar 2025 15:38:48 -0300
Subject: [PATCH 29/30] typo, refs`
---
docs/5.advanced/7.mcp.md | 2 +-
docs/7.references/3.context.md | 20 ++++++++++++++++++++
docs/context.rst | 1 +
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/docs/5.advanced/7.mcp.md b/docs/5.advanced/7.mcp.md
index b9c6360bd..6492fc747 100644
--- a/docs/5.advanced/7.mcp.md
+++ b/docs/5.advanced/7.mcp.md
@@ -65,7 +65,7 @@ If your server is running, but Cursor doesn't connect, try "Reload Window" in th
There is no official support for MCP in VSCode yet.
-There are two extensions available in repository to add MCP capabilities to Copilot. None of them worked out-of-the-box at the time of writing this document. If have better luck, please let us know in GitHub issues.
+There are two extensions available in repository to add MCP capabilities to Copilot. None of them worked out-of-the-box at the time of writing this document. If you have better luck, please let us know in GitHub issues.
- [Copilot MCP](https://marketplace.visualstudio.com/items?itemName=AutomataLabs.copilot-mcp)
- [MCP-Client](https://marketplace.visualstudio.com/items?itemName=m1self.mcp-client)
diff --git a/docs/7.references/3.context.md b/docs/7.references/3.context.md
index 8d40e250d..b5c9b0b09 100644
--- a/docs/7.references/3.context.md
+++ b/docs/7.references/3.context.md
@@ -65,6 +65,26 @@ description: "Context reference"
+
+
+## dipdup.context.McpContext
+
+class dipdup.context.McpContext(config, package, datasources, transactions, logger, server)
+Execution context of MCP tools, resources and prompts.
+
+- Parameters:
+
+config (DipDupConfig) – DipDup configuration
+package (DipDupPackage) – DipDup package
+datasources (dict[str, Datasource[Any]]) – Mapping of available datasources
+transactions (TransactionManager) – Transaction manager (low-level interface)
+logger (Logger) – Context-aware logger instance
+server (McpServer[Any]) – Running MCP server instance
+
+
+
+
+
## dipdup.context.DipDupContext.add_contract
diff --git a/docs/context.rst b/docs/context.rst
index d0183170e..013204e35 100644
--- a/docs/context.rst
+++ b/docs/context.rst
@@ -3,6 +3,7 @@
.. autoclass:: dipdup.context.DipDupContext
.. autoclass:: dipdup.context.HandlerContext
.. autoclass:: dipdup.context.HookContext
+.. autoclass:: dipdup.context.McpContext
.. automethod:: dipdup.context.DipDupContext.add_contract
.. automethod:: dipdup.context.DipDupContext.add_index
From 019cf49f983091139899942c8e6042a6e85a3e6e Mon Sep 17 00:00:00 2001
From: Lev Gorodetskiy
Date: Sat, 22 Mar 2025 13:14:48 -0300
Subject: [PATCH 30/30] docs
---
docs/1.getting-started/4.package.md | 1 +
docs/5.advanced/7.mcp.md | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/docs/1.getting-started/4.package.md b/docs/1.getting-started/4.package.md
index 880591377..1403c3c0e 100644
--- a/docs/1.getting-started/4.package.md
+++ b/docs/1.getting-started/4.package.md
@@ -21,6 +21,7 @@ The structure of the resulting package is the following:
| :file_folder: `hasura` | Arbitrary Hasura metadata to apply during configuration |
| :file_folder: `hooks` | User-defined callbacks to run manually or by schedule |
| :file_folder: `models` | DipDup ORM models to store data in the database |
+| :file_folder: `mcp` | Custom MCP rools and resources |
| :file_folder: `sql` | SQL scripts and queries to run manually or on specific events |
| :file_folder: `types` | Automatically generated Pydantic dataclasses for contract types |
| `dipdup.yaml` | Root DipDup config; can be expanded with env-specific files |
diff --git a/docs/5.advanced/7.mcp.md b/docs/5.advanced/7.mcp.md
index 6492fc747..756ed0f4b 100644
--- a/docs/5.advanced/7.mcp.md
+++ b/docs/5.advanced/7.mcp.md
@@ -19,7 +19,7 @@ There are three types of MCP primitives (citing from the docs):
- **Resources** are **application-controlled** and allow to expose data and content that can be read by clients and used as context for LLM interactions.
- **Tools** are **model-controlled** and enable servers to expose executable functionality to clients, such as interacting with external systems, performing computations, and taking actions in the real world.
-- **Prompts** are **user-controlled** enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs
+- **Prompts** are **user-controlled** and enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs
**DipDup** provides an MCP server with several built-in primitives to help LLMs understand the context of your project and the current state of the indexer. You can also implement your own tools and resources specific to your project.
@@ -65,9 +65,9 @@ If your server is running, but Cursor doesn't connect, try "Reload Window" in th
There is no official support for MCP in VSCode yet.
-There are two extensions available in repository to add MCP capabilities to Copilot. None of them worked out-of-the-box at the time of writing this document. If you have better luck, please let us know in GitHub issues.
+There are two extensions available in repository to add MCP capabilities to **Copilot** assistant. None of them worked out-of-the-box at the time of writing this document.
-- [Copilot MCP](https://marketplace.visualstudio.com/items?itemName=AutomataLabs.copilot-mcp)
+- [Copilot MCP](https://marketplace.visualstudio.com/items?itemName=AutomataLabs.copilot-mcp) (issue [#20](https://github.com/VikashLoomba/copilot-mcp/issues/20))
- [MCP-Client](https://marketplace.visualstudio.com/items?itemName=m1self.mcp-client)
### Claude Desktop
@@ -190,7 +190,7 @@ logging:
## Known issues
- **DipDup** doesn't support custom prompts at the moment.
-- **Cursor** fails to discover resources exposed by DipDup server. Tools work fine.
+- **Cursor** fails to discover resources exposed by DipDup server. Tools work fine. (0.47.8 tested)
- **VSCode** community extensions for MCP don't work out-of-the-box.
- **Claude Desktop** doesn't support SSE-based MCP servers; gateway tools are required.