1212from copy import copy
1313from dataclasses import field
1414from enum import Enum
15- from functools import reduce
15+ from functools import cached_property , reduce
1616from os import environ as env
1717from os .path import dirname
1818from pydoc import locate
2525from ruamel .yaml import YAML
2626from typing_extensions import Literal
2727
28+ from dipdup .datasources .subscription import BigMapSubscription , OriginationSubscription , Subscription , TransactionSubscription
2829from dipdup .enums import ReindexingAction , ReindexingReasonC
2930from dipdup .exceptions import ConfigInitializationException , ConfigurationError
3031from dipdup .utils import import_from , pascal_to_snake , snake_to_pascal
@@ -590,20 +591,33 @@ def template_values(self, value: Dict[str, str]) -> None:
590591 self ._template_values = value
591592
592593
594+ @dataclass
595+ class SubscriptionsMixin :
596+ def __post_init_post_parse__ (self ) -> None :
597+ self ._subscriptions : Set [Subscription ] = set ()
598+
599+ @property
600+ def subscriptions (self ) -> Set [Subscription ]:
601+ return self ._subscriptions
602+
603+
593604@dataclass
594605class IndexTemplateConfig (NameMixin ):
595606 kind = 'template'
596607 template : str
597608 values : Dict [str , str ]
609+ first_level : int = 0
610+ last_level : int = 0
598611
599612
600613@dataclass
601- class IndexConfig (TemplateValuesMixin , NameMixin , ParentMixin ['ResolvedIndexConfigT' ]):
614+ class IndexConfig (TemplateValuesMixin , NameMixin , SubscriptionsMixin , ParentMixin ['ResolvedIndexConfigT' ]):
602615 datasource : Union [str , TzktDatasourceConfig ]
603616
604617 def __post_init_post_parse__ (self ) -> None :
605618 TemplateValuesMixin .__post_init_post_parse__ (self )
606619 NameMixin .__post_init_post_parse__ (self )
620+ SubscriptionsMixin .__post_init_post_parse__ (self )
607621 ParentMixin .__post_init_post_parse__ (self )
608622
609623 @property
@@ -642,8 +656,8 @@ class OperationIndexConfig(IndexConfig):
642656
643657 :param datasource: Alias of index datasource in `datasources` section
644658 :param contracts: Aliases of contracts being indexed in `contracts` section
645- :param first_level: First block to process (use with `--oneshot` run argument )
646- :param last_level: Last block to process (use with `--oneshot` run argument )
659+ :param first_level: First block to process (one time sync )
660+ :param last_level: Last block to process (one time sync )
647661 :param handlers: List of indexer handlers
648662 """
649663
@@ -655,6 +669,33 @@ class OperationIndexConfig(IndexConfig):
655669 first_level : int = 0
656670 last_level : int = 0
657671
672+ @cached_property
673+ def entrypoint_filter (self ) -> Set [Optional [str ]]:
674+ entrypoints = set ()
675+ for handler_config in self .handlers :
676+ for pattern_config in handler_config .pattern :
677+ if isinstance (pattern_config , OperationHandlerTransactionPatternConfig ):
678+ entrypoints .add (pattern_config .entrypoint )
679+ return set (entrypoints )
680+
681+ @cached_property
682+ def address_filter (self ) -> Set [str ]:
683+ addresses = set ()
684+ for handler_config in self .handlers :
685+ for pattern_config in handler_config .pattern :
686+ if isinstance (pattern_config , OperationHandlerTransactionPatternConfig ):
687+ if isinstance (pattern_config .source , ContractConfig ):
688+ addresses .add (pattern_config .source .address )
689+ elif isinstance (pattern_config .source , str ):
690+ raise ConfigInitializationException
691+
692+ if isinstance (pattern_config .destination , ContractConfig ):
693+ addresses .add (pattern_config .destination .address )
694+ elif isinstance (pattern_config .destination , str ):
695+ raise ConfigInitializationException
696+
697+ return addresses
698+
658699
659700@dataclass
660701class BigMapHandlerConfig (HandlerConfig , kind = 'handler' ):
@@ -809,11 +850,15 @@ class JobConfig(NameMixin):
809850 hook : Union [str , 'HookConfig' ]
810851 crontab : Optional [str ] = None
811852 interval : Optional [int ] = None
853+ daemon : bool = False
812854 args : Dict [str , Any ] = field (default_factory = dict )
813855
814856 def __post_init_post_parse__ (self ):
815- if int (bool (self .crontab )) + int (bool (self .interval )) != 1 :
816- raise ConfigurationError ('Either `interval` or `crontab` field must be specified' )
857+ if self .crontab and self .interval :
858+ raise ConfigurationError ('Only one of `crontab` and `interval` can be specified' )
859+ elif not (self .crontab or self .interval or self .daemon ):
860+ raise ConfigurationError ('One of `crontab`, `interval` or `daemon` must be specified' )
861+
817862 NameMixin .__post_init_post_parse__ (self )
818863
819864 @property
@@ -878,10 +923,12 @@ def _args_with_context(self) -> Dict[str, str]:
878923
879924@dataclass
880925class AdvancedConfig :
926+ reindex : Dict [ReindexingReasonC , ReindexingAction ] = Field (default_factory = dict )
927+ oneshot : bool = False
881928 postpone_jobs : bool = False
882929 skip_hasura : bool = False
883930 early_realtime : bool = False
884- reindex : Dict [ ReindexingReasonC , ReindexingAction ] = Field ( default_factory = dict )
931+ merge_subscriptions : bool = False
885932
886933
887934@dataclass
@@ -945,6 +992,17 @@ def package_path(self, value: str):
945992 raise ConfigInitializationException
946993 self ._package_path = value
947994
995+ @property
996+ def oneshot (self ) -> bool :
997+ syncable_indexes = tuple (c for c in self .indexes .values () if not isinstance (c , HeadIndexConfig ))
998+ oneshot_indexes = tuple (c for c in syncable_indexes if c .last_level )
999+ if not oneshot_indexes :
1000+ return False
1001+ elif len (oneshot_indexes ) == len (syncable_indexes ):
1002+ return True
1003+ else :
1004+ raise ConfigurationError ('Either all or none of indexes can have `last_level` field set' )
1005+
9481006 @classmethod
9491007 def load (
9501008 cls ,
@@ -962,7 +1020,7 @@ def load(
9621020 with open (filename ) as file :
9631021 raw_config = file .read ()
9641022
965- _logger .info ('Substituting environment variables' )
1023+ _logger .debug ('Substituting environment variables' )
9661024 for match in re .finditer (ENV_VARIABLE_REGEX , raw_config ):
9671025 variable , default_value = match .group (1 ), match .group (2 )
9681026 config_environment [variable ] = default_value
@@ -1034,7 +1092,7 @@ def initialize(self, skip_imports: bool = False) -> None:
10341092 if index_config .name in self ._imports_resolved :
10351093 continue
10361094
1037- _logger .info ('Loading callbacks and typeclasses of index `%s`' , index_config .name )
1095+ _logger .debug ('Loading callbacks and typeclasses of index `%s`' , index_config .name )
10381096
10391097 if isinstance (index_config , IndexTemplateConfig ):
10401098 raise ConfigInitializationException
@@ -1107,7 +1165,7 @@ def get_pattern_type(pattern_items: Sequence[HandlerPatternConfigT]) -> str:
11071165 )
11081166
11091167 def _resolve_template (self , template_config : IndexTemplateConfig ) -> None :
1110- _logger .info ('Resolving index config `%s` from template `%s`' , template_config .name , template_config .template )
1168+ _logger .debug ('Resolving index config `%s` from template `%s`' , template_config .name , template_config .template )
11111169
11121170 template = self .get_template (template_config .template )
11131171 raw_template = json .dumps (template , default = pydantic_encoder )
@@ -1124,6 +1182,9 @@ def _resolve_template(self, template_config: IndexTemplateConfig) -> None:
11241182 new_index_config .template_values = template_config .values
11251183 new_index_config .parent = template
11261184 new_index_config .name = template_config .name
1185+ if not isinstance (new_index_config , HeadIndexConfig ):
1186+ new_index_config .first_level |= template_config .first_level
1187+ new_index_config .last_level |= template_config .last_level
11271188 self .indexes [template_config .name ] = new_index_config
11281189
11291190 def _resolve_templates (self ) -> None :
@@ -1136,12 +1197,49 @@ def _resolve_links(self) -> None:
11361197 if name in self ._links_resolved :
11371198 continue
11381199 self ._resolve_index_links (index_config )
1200+ # TODO: Not exactly link resolving, move somewhere else
1201+ self ._resolve_index_subscriptions (index_config )
11391202 self ._links_resolved .add (index_config .name )
11401203
11411204 for job_config in self .jobs .values ():
11421205 if isinstance (job_config .hook , str ):
11431206 job_config .hook = self .hooks [job_config .hook ]
11441207
1208+ def _resolve_index_subscriptions (self , index_config : IndexConfigT ) -> None :
1209+ if isinstance (index_config , IndexTemplateConfig ):
1210+ return
1211+ if index_config .subscriptions :
1212+ return
1213+
1214+ if isinstance (index_config , OperationIndexConfig ):
1215+ if self .advanced .merge_subscriptions :
1216+ index_config .subscriptions .add (TransactionSubscription ())
1217+ else :
1218+ for contract_config in index_config .contracts or ():
1219+ address = cast (ContractConfig , contract_config ).address
1220+ index_config .subscriptions .add (TransactionSubscription (address = address ))
1221+
1222+ for handler_config in index_config .handlers :
1223+ for pattern_config in handler_config .pattern :
1224+ if isinstance (pattern_config , OperationHandlerOriginationPatternConfig ):
1225+ index_config .subscriptions .add (OriginationSubscription ())
1226+ break
1227+
1228+ elif isinstance (index_config , BigMapIndexConfig ):
1229+ if self .advanced .merge_subscriptions :
1230+ index_config .subscriptions .add (BigMapSubscription ())
1231+ else :
1232+ for big_map_handler_config in index_config .handlers :
1233+ address , path = big_map_handler_config .contract_config .address , big_map_handler_config .path
1234+ index_config .subscriptions .add (BigMapSubscription (address = address , path = path ))
1235+
1236+ # NOTE: HeadSubscription is always enabled
1237+ elif isinstance (index_config , HeadIndexConfig ):
1238+ pass
1239+
1240+ else :
1241+ raise NotImplementedError (f'Index kind `{ index_config .kind } ` is not supported' )
1242+
11451243 def _resolve_index_links (self , index_config : IndexConfigT ) -> None :
11461244 """Resolve contract and datasource configs by aliases"""
11471245
0 commit comments