class Factory: """Interface to build units for an army. Army side is provided at instanciation. Each call providing unit type and position will use loaded stats to add a new unit to the underlying army. """ side: int units: List[Unit] = dfield(default_factory=list) _registery = {} @classmethod @tools(log="registered {name}") def register(cls, name, prototype): """Register prototype for given unit type name.""" cls._registery[name] = prototype @property def army(self): """Provide army built from created units.""" return Army(self.units) def __call__(self, name, coords, strategy, is_centurion=False): try: base = self._registery[name] except KeyError: tools(log=f"Unregistered name: {name}")() base = UnitBase(0, 0, 0, 0) field = UnitField(self.side, coords, is_centurion) strat = Strategy(*strategy) self.units.append(Unit(base, field, strat)) return self.units[-1]
class ProfileSearch(SumoSearch): """Search over User Profiles.""" group_ids: list[int] = dfield(default_factory=list) def get_index(self): return ProfileDocument.Index.read_alias def get_fields(self): return ["username", "name"] def get_highlight_fields_options(self): return [] def get_filter(self): return DSLQ( "boosting", positive=self.build_query(), negative=DSLQ( "bool", must_not=DSLQ("terms", group_ids=self.group_ids), ), negative_boost=0.5, ) def make_result(self, hit): return { "type": "user", "avatar": getattr(hit, "avatar", None), "username": hit.username, "name": getattr(hit, "name", ""), "user_id": hit.meta.id, }
class CompoundSearch(SumoSearch): """Combine a number of SumoSearch classes into one search.""" _children: list[SumoSearch] = dfield(default_factory=list, init=False) _parse_query: bool = True @property def parse_query(self): return self._parse_query @parse_query.setter def parse_query(self, value): """Set value of parse_query across all children.""" self._parse_query = value for child in self._children: child.parse_query = value def add(self, child): """Add a SumoSearch instance to search over. Chainable.""" self._children.append(child) def _from_children(self, name): """ Get an attribute from all children. Will flatten lists. """ value = [] for child in self._children: attr = getattr(child, name)() if isinstance(attr, list): # if the attribute's value is itself a list, unpack it value = [*value, *attr] else: value.append(attr) return value def get_index(self): return ",".join(self._from_children("get_index")) def get_fields(self): return self._from_children("get_fields") def get_highlight_fields_options(self): return self._from_children("get_highlight_fields_options") def get_filter(self): # `should` with `minimum_should_match=1` acts like an OR filter return DSLQ("bool", should=self._from_children("get_filter"), minimum_should_match=1) def make_result(self, hit): index = hit.meta.index for child in self._children: if same_base_index(index, child.get_index()): return child.make_result(hit)
class SumoSearch(SumoSearchInterface): """Base class for search classes. Implements the run() function, which will perform the search. Child classes should define values for the various abstract properties this class inherits, relevant to the documents the child class is searching over. """ total: int = dfield(default=0, init=False) hits: list[AttrDict] = dfield(default_factory=list, init=False) results: list[dict] = dfield(default_factory=list, init=False) last_key: Union[int, slice, None] = dfield(default=None, init=False) query: str = "" default_operator: str = "AND" def __len__(self): return self.total @overload def __getitem__(self, key: int) -> dict: ... @overload def __getitem__(self, key: slice) -> list[dict]: ... def __getitem__(self, key): if self.last_key is None or self.last_key != key: self.run(key=key) if isinstance(key, int): # if key is an int, then self.results will be a list containing a single result # return the result, rather than a 1-length list return self.results[0] return self.results def build_query(self): """Build a query to search over a specific set of documents.""" return DSLQ( "simple_query_string", query=self.query, default_operator=self.default_operator, fields=self.get_fields(), # everything apart from WHITESPACE as that interferes with char mappings # and synonyms with whitespace in them by breaking up the phrase into tokens, # before they have a chance to go through the filter: flags="AND|ESCAPE|FUZZY|NEAR|NOT|OR|PHRASE|PRECEDENCE|PREFIX|SLOP", ) def run(self, key: Union[int, slice] = slice(0, settings.SEARCH_RESULTS_PER_PAGE)): """Perform search, placing the results in `self.results`, and the total number of results (across all pages) in `self.total`. Chainable.""" search = DSLSearch( using=es7_client(), index=self.get_index()).params(**settings.ES7_SEARCH_PARAMS) # add the search class' filter search = search.query(self.get_filter()) # add highlights for the search class' highlight_fields for highlight_field, options in self.get_highlight_fields_options(): search = search.highlight(highlight_field, **options) # slice search search = search[key] # perform search self.hits = search.execute().hits self.last_key = key self.total = self.hits.total.value self.results = [self.make_result(hit) for hit in self.hits] return self
class PROXIDataset: accession: str = dfield(default=None) title: str = dfield(default=None) summary: str = dfield(default=None) instruments: List[str] = dfield(default_factory=list) identifiers: List[str] = dfield(default_factory=list) species: List[str] = dfield(default_factory=list) modifications: List[str] = dfield(default_factory=list) contacts: List[str] = dfield(default_factory=list) publications: List[str] = dfield(default_factory=list) keywords: List[str] = dfield(default_factory=list) dataset_link: str = dfield(default=None) dataset_files: List[PROXIDataFile] = dfield(default_factory=list) links: List[str] = dfield(default_factory=list) def to_dict(self): d = dataclasses.asdict(self) d['datasetLinks'] = d.pop("dataset_links", None) # TODO: Make this consistent whether or not it is a dict or an instance d['dataFiles'] = [ PROXIDataFile(f['uri'], FileType(**f['file_type'])).to_dict() for f in d.pop('dataset_files', []) ] return d @classmethod def from_dict(cls, d): d = d.copy() d['dataset_links'] = d.pop('datasetLinks', None) d['dataset_files'] = [ PROXIDataFile.from_dict(f) for f in d.pop('datasetFiles', []) ] return cls(**d) @classmethod def new(cls, dataset_id): self = cls() self.accession = dataset_id self.title = dataset_id self.summary = dataset_id return self