Skip to content

Commit 65b9ea3

Browse files
SubscriptionManager class, bugfixes (#106)
1 parent 1c43ebf commit 65b9ea3

File tree

15 files changed

+238
-101
lines changed

15 files changed

+238
-101
lines changed

src/dipdup/__main__.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
1-
import logging
2-
31
from dipdup.cli import cli
4-
from dipdup.exceptions import DipDupError
52

63
if __name__ == '__main__':
7-
try:
8-
cli(prog_name='dipdup', standalone_mode=False) # type: ignore
9-
except KeyboardInterrupt:
10-
pass
11-
except DipDupError as e:
12-
logging.critical(e.__repr__())
13-
logging.info(e.format())
14-
quit(e.exit_code)
4+
cli(prog_name='dipdup', standalone_mode=False) # type: ignore

src/dipdup/cli.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
from dataclasses import dataclass
5+
from functools import wraps
56
from os.path import dirname, exists, join
67
from typing import List, cast
78

@@ -16,7 +17,7 @@
1617
from dipdup.codegen import DEFAULT_DOCKER_ENV_FILE, DEFAULT_DOCKER_IMAGE, DEFAULT_DOCKER_TAG, DipDupCodeGenerator
1718
from dipdup.config import DipDupConfig, LoggingConfig, PostgresDatabaseConfig
1819
from dipdup.dipdup import DipDup
19-
from dipdup.exceptions import ConfigurationError, MigrationRequiredError
20+
from dipdup.exceptions import ConfigurationError, DipDupError, MigrationRequiredError
2021
from dipdup.hasura import HasuraGateway
2122
from dipdup.utils import set_decimal_context, tortoise_wrapper
2223

@@ -30,6 +31,21 @@ class CLIContext:
3031
logging_config: LoggingConfig
3132

3233

34+
def cli_wrapper(fn):
35+
@wraps(fn)
36+
async def wrapper(*args, **kwargs):
37+
try:
38+
return await fn(*args, **kwargs)
39+
except KeyboardInterrupt:
40+
pass
41+
except DipDupError as e:
42+
_logger.critical(e.__repr__())
43+
_logger.info(e.format())
44+
quit(e.exit_code)
45+
46+
return wrapper
47+
48+
3349
def init_sentry(config: DipDupConfig) -> None:
3450
if not config.sentry:
3551
return
@@ -59,6 +75,7 @@ def init_sentry(config: DipDupConfig) -> None:
5975
@click.option('--env-file', '-e', type=str, multiple=True, help='Path to .env file', default=[])
6076
@click.option('--logging-config', '-l', type=str, help='Path to logging YAML config', default='logging.yml')
6177
@click.pass_context
78+
@cli_wrapper
6279
async def cli(ctx, config: List[str], env_file: List[str], logging_config: str):
6380
try:
6481
path = join(os.getcwd(), logging_config)
@@ -96,6 +113,7 @@ async def cli(ctx, config: List[str], env_file: List[str], logging_config: str):
96113
@click.option('--reindex', is_flag=True, help='Drop database and start indexing from scratch')
97114
@click.option('--oneshot', is_flag=True, help='Synchronize indexes wia REST and exit without starting WS connection')
98115
@click.pass_context
116+
@cli_wrapper
99117
async def run(ctx, reindex: bool, oneshot: bool) -> None:
100118
config: DipDupConfig = ctx.obj.config
101119
config.initialize()
@@ -106,6 +124,7 @@ async def run(ctx, reindex: bool, oneshot: bool) -> None:
106124

107125
@cli.command(help='Initialize new dipdup project')
108126
@click.pass_context
127+
@cli_wrapper
109128
async def init(ctx):
110129
config: DipDupConfig = ctx.obj.config
111130
config.pre_initialize()
@@ -115,6 +134,7 @@ async def init(ctx):
115134

116135
@cli.command(help='Migrate project to the new spec version')
117136
@click.pass_context
137+
@cli_wrapper
118138
async def migrate(ctx):
119139
def _bump_spec_version(spec_version: str):
120140
for config_path in ctx.obj.config_paths:
@@ -141,12 +161,14 @@ def _bump_spec_version(spec_version: str):
141161

142162
@cli.command(help='Clear development request cache')
143163
@click.pass_context
164+
@cli_wrapper
144165
async def clear_cache(ctx):
145166
FileCache('dipdup', flag='cs').clear()
146167

147168

148169
@cli.group()
149170
@click.pass_context
171+
@cli_wrapper
150172
async def docker(ctx):
151173
...
152174

@@ -156,20 +178,23 @@ async def docker(ctx):
156178
@click.option('--tag', '-t', type=str, help='DipDup Docker tag', default=DEFAULT_DOCKER_TAG)
157179
@click.option('--env-file', '-e', type=str, help='Path to env_file', default=DEFAULT_DOCKER_ENV_FILE)
158180
@click.pass_context
181+
@cli_wrapper
159182
async def docker_init(ctx, image: str, tag: str, env_file: str):
160183
config: DipDupConfig = ctx.obj.config
161184
await DipDupCodeGenerator(config, {}).generate_docker(image, tag, env_file)
162185

163186

164187
@cli.group()
165188
@click.pass_context
189+
@cli_wrapper
166190
async def hasura(ctx):
167191
...
168192

169193

170194
@hasura.command(name='configure', help='Configure Hasura GraphQL Engine')
171195
@click.option('--reset', is_flag=True, help='Reset metadata before configuring')
172196
@click.pass_context
197+
@cli_wrapper
173198
async def hasura_configure(ctx, reset: bool):
174199
config: DipDupConfig = ctx.obj.config
175200
url = config.database.connection_string

src/dipdup/codegen.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
OperationIndexConfig,
2525
TzktDatasourceConfig,
2626
)
27-
from dipdup.datasources import DatasourceT
27+
from dipdup.datasources.datasource import Datasource
2828
from dipdup.datasources.tzkt.datasource import TzktDatasource
2929
from dipdup.exceptions import ConfigurationError
3030
from dipdup.utils import import_submodules, mkdir_p, pascal_to_snake, snake_to_pascal, touch, write
@@ -68,7 +68,7 @@ def load_template(name: str) -> Template:
6868
class DipDupCodeGenerator:
6969
"""Generates package based on config, invoked from `init` CLI command"""
7070

71-
def __init__(self, config: DipDupConfig, datasources: Dict[DatasourceConfigT, DatasourceT]) -> None:
71+
def __init__(self, config: DipDupConfig, datasources: Dict[DatasourceConfigT, Datasource]) -> None:
7272
self._logger = logging.getLogger('dipdup.codegen')
7373
self._config = config
7474
self._datasources = datasources

src/dipdup/config.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from enum import Enum
1111
from os import environ as env
1212
from os.path import dirname
13-
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union, cast
13+
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union, cast
1414
from urllib.parse import urlparse
1515

1616
from pydantic import Field, validator
@@ -285,6 +285,24 @@ def initialize_storage_cls(self, package: str, module_name: str) -> None:
285285
self.storage_type_cls = storage_type_cls
286286

287287

288+
@dataclass
289+
class ParentMixin:
290+
"""`parent` field for index and template configs"""
291+
292+
def __post_init_post_parse__(self):
293+
self._parent: Optional['IndexConfig'] = None
294+
295+
@property
296+
def parent(self) -> Optional['IndexConfig']:
297+
return self._parent
298+
299+
@parent.setter
300+
def parent(self, config: 'IndexConfig') -> None:
301+
if self._parent:
302+
raise RuntimeError('Can\'t unset parent once set')
303+
self._parent = config
304+
305+
288306
@dataclass
289307
class ParameterTypeMixin:
290308
"""`parameter_type_cls` field"""
@@ -459,10 +477,11 @@ def originated_contract_config(self) -> ContractConfig:
459477

460478

461479
@dataclass
462-
class HandlerConfig:
480+
class HandlerConfig(NameMixin):
463481
callback: str
464482

465483
def __post_init_post_parse__(self):
484+
super().__post_init_post_parse__()
466485
self._callback_fn = None
467486
if self.callback in (ROLLBACK_HANDLER, CONFIGURE_HANDLER):
468487
raise ConfigurationError(f'`{self.callback}` callback name is reserved')
@@ -509,12 +528,13 @@ def template_values(self, value: Dict[str, str]) -> None:
509528

510529

511530
@dataclass
512-
class IndexConfig(TemplateValuesMixin, NameMixin):
531+
class IndexConfig(TemplateValuesMixin, NameMixin, ParentMixin):
513532
datasource: Union[str, TzktDatasourceConfig]
514533

515534
def __post_init_post_parse__(self) -> None:
516535
TemplateValuesMixin.__post_init_post_parse__(self)
517536
NameMixin.__post_init_post_parse__(self)
537+
ParentMixin.__post_init_post_parse__(self)
518538

519539
def hash(self) -> str:
520540
config_json = json.dumps(self, default=pydantic_encoder)
@@ -532,10 +552,11 @@ def datasource_config(self) -> TzktDatasourceConfig:
532552
class OperationIndexConfig(IndexConfig):
533553
"""Operation index config
534554
535-
:param datasource: Alias of datasource in `datasources` block
536-
:param contract: Alias of contract to fetch operations for
537-
:param first_block: First block to process
538-
:param last_block: Last block to process
555+
:param datasource: Alias of index datasource in `datasources` section
556+
:param contracts: Aliases of contracts being indexed in `contracts` section
557+
:param stateless: Makes index dynamic. DipDup will synchronize index from the first block on every run
558+
:param first_block: First block to process (use with `--oneshot` run argument)
559+
:param last_block: Last block to process (use with `--oneshot` run argument)
539560
:param handlers: List of indexer handlers
540561
"""
541562

@@ -557,6 +578,15 @@ def contract_configs(self) -> List[ContractConfig]:
557578
raise RuntimeError('Config is not initialized')
558579
return cast(List[ContractConfig], self.contracts)
559580

581+
@property
582+
def entrypoints(self) -> Set[str]:
583+
entrypoints = set()
584+
for handler in self.handlers:
585+
for pattern in handler.pattern:
586+
if isinstance(pattern, OperationHandlerTransactionPatternConfig) and pattern.entrypoint:
587+
entrypoints.add(pattern.entrypoint)
588+
return entrypoints
589+
560590

561591
@dataclass
562592
class BigMapHandlerConfig(HandlerConfig):
@@ -611,7 +641,7 @@ def contracts(self) -> List[ContractConfig]:
611641

612642

613643
@dataclass
614-
class IndexTemplateConfig:
644+
class IndexTemplateConfig(ParentMixin):
615645
kind = 'template'
616646
template: str
617647
values: Dict[str, str]
@@ -692,12 +722,12 @@ class DipDupConfig:
692722
spec_version: str
693723
package: str
694724
datasources: Dict[str, DatasourceConfigT]
725+
database: Union[SqliteDatabaseConfig, PostgresDatabaseConfig] = SqliteDatabaseConfig(kind='sqlite')
695726
contracts: Dict[str, ContractConfig] = Field(default_factory=dict)
696727
indexes: Dict[str, IndexConfigT] = Field(default_factory=dict)
697728
templates: Dict[str, IndexConfigTemplateT] = Field(default_factory=dict)
698-
database: Union[SqliteDatabaseConfig, PostgresDatabaseConfig] = SqliteDatabaseConfig(kind='sqlite')
729+
jobs: Dict[str, JobConfig] = Field(default_factory=dict)
699730
hasura: Optional[HasuraConfig] = None
700-
jobs: Optional[Dict[str, JobConfig]] = None
701731
sentry: Optional[SentryConfig] = None
702732

703733
def __post_init_post_parse__(self):
@@ -721,20 +751,27 @@ def validate(self) -> None:
721751
raise ConfigurationError('SQLite DB engine is not supported by Hasura')
722752

723753
def get_contract(self, name: str) -> ContractConfig:
754+
if name.startswith('<') and name.endswith('>'):
755+
raise ConfigurationError(f'`{name}` variable of index template is not set')
756+
724757
try:
725758
return self.contracts[name]
726759
except KeyError as e:
727760
raise ConfigurationError(f'Contract `{name}` not found in `contracts` config section') from e
728761

729762
def get_datasource(self, name: str) -> DatasourceConfigT:
763+
if name.startswith('<') and name.endswith('>'):
764+
raise ConfigurationError(f'`{name}` variable of index template is not set')
765+
730766
try:
731767
return self.datasources[name]
732768
except KeyError as e:
733769
raise ConfigurationError(f'Datasource `{name}` not found in `datasources` config section') from e
734770

735771
def get_template(self, name: str) -> IndexConfigTemplateT:
736-
if not self.templates:
737-
raise ConfigurationError('`templates` section is missing')
772+
if name.startswith('<') and name.endswith('>'):
773+
raise ConfigurationError(f'`{name}` variable of index template is not set')
774+
738775
try:
739776
return self.templates[name]
740777
except KeyError as e:
@@ -772,6 +809,7 @@ def resolve_index_templates(self) -> None:
772809
json_template = json.loads(raw_template)
773810
new_index_config = template.__class__(**json_template)
774811
new_index_config.template_values = index_config.values
812+
new_index_config.parent = index_config.parent
775813
self.indexes[index_name] = new_index_config
776814

777815
def _pre_initialize_index(self, index_name: str, index_config: IndexConfigT) -> None:
@@ -831,6 +869,8 @@ def pre_initialize(self) -> None:
831869
contract_config.name = name
832870
for name, datasource_config in self.datasources.items():
833871
datasource_config.name = name
872+
for name, job_config in self.jobs.items():
873+
job_config.name = name
834874

835875
self.resolve_index_templates()
836876
for index_name, index_config in self.indexes.items():

src/dipdup/context.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
from tortoise import Tortoise
77
from tortoise.transactions import in_transaction
88

9-
from dipdup.config import ContractConfig, DipDupConfig, IndexTemplateConfig, PostgresDatabaseConfig
10-
from dipdup.datasources import DatasourceT
9+
from dipdup.config import ContractConfig, DipDupConfig, IndexConfig, IndexTemplateConfig, PostgresDatabaseConfig
10+
from dipdup.datasources.datasource import Datasource
1111
from dipdup.exceptions import ContractAlreadyExistsError, IndexAlreadyExistsError
1212
from dipdup.utils import FormattedLogger
1313

1414

15-
# TODO: Dataclasses are cool, everyone loves them. Resolve issue with pydantic in HandlerContext.
15+
# TODO: Dataclasses are cool, everyone loves them. Resolve issue with pydantic serialization.
1616
class DipDupContext:
1717
def __init__(
1818
self,
19-
datasources: Dict[str, DatasourceT],
19+
datasources: Dict[str, Datasource],
2020
config: DipDupConfig,
2121
) -> None:
2222
self.datasources = datasources
@@ -74,16 +74,18 @@ class HandlerContext(DipDupContext):
7474

7575
def __init__(
7676
self,
77-
datasources: Dict[str, DatasourceT],
77+
datasources: Dict[str, Datasource],
7878
config: DipDupConfig,
7979
logger: FormattedLogger,
8080
template_values: Optional[Dict[str, str]],
81-
datasource: DatasourceT,
81+
datasource: Datasource,
82+
index_config: IndexConfig,
8283
) -> None:
8384
super().__init__(datasources, config)
8485
self.logger = logger
8586
self.template_values = template_values
8687
self.datasource = datasource
88+
self.index_config = index_config
8789

8890
def add_contract(self, name: str, address: str, typename: Optional[str] = None) -> None:
8991
if name in self.config.contracts:
@@ -102,21 +104,38 @@ def add_index(self, name: str, template: str, values: Dict[str, Any]) -> None:
102104
template=template,
103105
values=values,
104106
)
107+
# NOTE: Notify datasource to subscribe to operations by entrypoint if enabled in index config
108+
self.config.indexes[name].parent = self.index_config
105109
self._updated = True
106110

107111

108-
class RollbackHandlerContext(HandlerContext):
109-
template_values: None
112+
class JobContext(DipDupContext):
113+
"""Job handler context."""
110114

111115
def __init__(
112116
self,
113-
datasources: Dict[str, DatasourceT],
117+
datasources: Dict[str, Datasource],
114118
config: DipDupConfig,
115119
logger: FormattedLogger,
116-
datasource: DatasourceT,
120+
) -> None:
121+
super().__init__(datasources, config)
122+
self.logger = logger
123+
124+
# TODO: Spawning indexes from jobs?
125+
126+
127+
class RollbackHandlerContext(DipDupContext):
128+
def __init__(
129+
self,
130+
datasources: Dict[str, Datasource],
131+
config: DipDupConfig,
132+
logger: FormattedLogger,
133+
datasource: Datasource,
117134
from_level: int,
118135
to_level: int,
119136
) -> None:
120-
super().__init__(datasources, config, logger, None, datasource)
137+
super().__init__(datasources, config)
138+
self.logger = logger
139+
self.datasource = datasource
121140
self.from_level = from_level
122141
self.to_level = to_level

0 commit comments

Comments
 (0)