def _get_cfg(keyname, can_raise=True): try: ns, key = [x.strip() for x in keyname.strip().split('.') if x] except ValueError: raise exceptions.APIError(utils.this_function(), "Invalid setting '{}'".format(keyname)) cfg = config.config ctx = utils.get_context() client_space = False if ns.lower() == 'this': client_space = True ns = ctx['name'] if ns.lower in config.ConfigNode.default_namespaces: raise exceptions.APIError( utils.this_function(), "Client name '{}' coincides with default namespace '{}'. Please use a different client name" .format(ns, ns.lower())) with cfg.tmp_config(ns, ctx['config']): if not cfg.key_exists(ns, key): if not client_space or (client_space and can_raise): raise exceptions.SettingsError( utils.this_function(), "Setting with key '{}' does not exist".format(keyname)) return ns, key
def update_item(item_type: enums.ItemType = enums.ItemType.Gallery, item: dict = {}, options: dict = {}): """ Update an existing item Args: item_type: type of item to create item: item messeage object Returns: bool indicating whether item was updated """ if not item: raise exceptions.APIError(utils.this_function(), "Item must be a message object") if not item.get('id'): raise exceptions.APIError(utils.this_function(), "Item must have a valid id") item_type = enums.ItemType.get(item_type) db_msg, db_model = item_type._msg_and_model() db_obj = db_msg.from_json(item, ignore_empty=False, skip_updating_existing=False) status = database_cmd.UpdateItem().main(db_obj, options=options) return message.Identity('status', status)
def new_item(item_type: enums.ItemType = enums.ItemType.Gallery, item: dict = {}, options: dict = {}): """ Create a new item and add it to the database Args: item_type: type of item to create item: item messeage object Returns: [] |async command| """ if not item: raise exceptions.APIError(utils.this_function(), "item must be a message object") if item.get('id', False) and not constants.dev: raise exceptions.APIError(utils.this_function(), "cannot create item with an id") item_type = enums.ItemType.get(item_type) db_msg, db_model = item_type._msg_and_model() db_obj = db_msg.from_json(item) cmd_id = database_cmd.AddItem(services.AsyncService.generic).run( db_obj, options=options) return message.Identity('command_id', cmd_id)
def translate(t_id: str, locale: str = None, default: str = None, placeholder: str = {}, count: int = None): """ Get a translation by translation id. Raises error if a default value was not provided and no translation was found. You can find more about translations :ref:`here <Translations>`. Args: t_id: translation id locale: locale to get translations from (will override default locale) default: default text when no translation was found placeholder: ? count: pluralization Returns: string .. seealso:: :func:`.get_locales` """ kwargs = {} trs = default kwargs["locale"] = helpers._get_locale(locale).lower() if placeholder: kwargs.update(placeholder), if count is not None: kwargs["count"] = count if default: kwargs["default"] = default if not t_id and default is None: raise exceptions.APIError(utils.this_function(), "Invalid translation id: {}".format(t_id)) elif t_id: try: trs = i18n.t(t_id, **kwargs) except KeyError as e: if default is None: raise exceptions.APIError( utils.this_function(), "Translation id '{}' not found".format(t_id)) except i18n.loaders.loader.I18nFileLoadError as e: if default is None: log.exception( "Failed to load translation file '{}' with key '{}'". format( locale if locale else config.translation_locale.value, t_id)) raise exceptions.APIError( utils.this_function(), "Failed to load translation file: {}".format(e.args)) return message.Identity("translation", trs)
def get_command_value(command_ids: list): """ Get the returned command value Args: command_ids: list of command ids Returns: ``` { command_id : value } ``` """ _command_msg(command_ids) values = {} for i in command_ids: cmd = Service.get_command(i) if cmd.state not in (command.CommandState.finished, command.CommandState.stopped): if cmd.state == command.CommandState.failed: raise exceptions.CommandError(utils.this_function(), "Command with ID '{}' has failed".format(i)) raise exceptions.CommandError(utils.this_function(), "Command with ID '{}' has not finished running".format(i)) if isinstance(cmd.value, message.CoreMessage): values[i] = cmd.value.json_friendly(include_key=False) else: values[i] = cmd.value if constants.debug: cmd._log_stats(arrow.now()) return message.Identity('command_value', values)
def get_item(item_type: enums.ItemType = enums.ItemType.Gallery, item_id: int = 0): """ Get item Args: item_type: type of item to get item_id: id of item Returns: item message object """ if not item_id: raise exceptions.APIError( utils.this_function(), f"A valid item id is required, not {item_id}") item_type = enums.ItemType.get(item_type) db_msg, db_model = item_type._msg_and_model() item = database_cmd.GetModelItems().run(db_model, {item_id})[0] if not item: raise exceptions.DatabaseItemNotFoundError( utils.this_function(), "'{}' with id '{}' was not found".format(item_type.name, item_id)) return db_msg(item)
def get_context(self, user=None, password=None): "Creates or retrieves existing context object for this client" s = constants.db_session() user_obj = None if user or password: log.d("Client provided credentials, authenticating...") if user == constants.super_user_name and not config.disable_default_user.value: log.d("Authenticating with default user") user_obj = s.query(db.User).filter( db.User.role == db.User.Role.default).one() else: user_obj = s.query( db.User).filter(db.User.name == user).one_or_none() if not user_obj or (not user_obj.role == db.User.Role.guest and not user_obj.password == password): raise exceptions.AuthWrongCredentialsError( utils.this_function(), "Wrong credentials") else: log.d("Client did not provide credentials") if not config.allow_guests.value: log.d("Guests are disallowed on this server") raise exceptions.AuthRequiredError( utils.this_function(), "Authentication is required") log.d("Authencticating as guest") user_obj = db.User(role=db.User.Role.guest) self.context['user'] = user_obj self.context['adresss'] = self._address if not self.context['user'].context_id: self.context['user'].context_id = uuid.uuid4().hex self.context['config'] = {} log.d("Client accepted") self._accepted = True
def get_related_items( item_type: enums.ItemType = enums.ItemType.Gallery, item_id: int = 0, related_type: enums.ItemType = enums.ItemType.Page, limit: int = 100, offset: int = None, ): """ Get item related to given item Args: item_type: parent item item_id: id of parent item related_type: child item limit: limit the amount of items returned offset: offset the results by n items Returns: .. code-block:: guess [ related item message object, ... ] """ if not item_id: raise exceptions.APIError(utils.this_function(), "item_id must be a valid item id") item_type = enums.ItemType.get(item_type) related_type = enums.ItemType.get(related_type) _, parent_model = item_type._msg_and_model() child_msg, child_model = related_type._msg_and_model() col = db.relationship_column(parent_model, child_model) if not col: raise exceptions.APIError( utils.this_function(), "{} has no relationship with {}".format(related_type, item_type)) s = constants.db_session() q = s.query(child_model.id).join(col).filter(parent_model.id == item_id) if offset: q = q.offset(offset) item_ids = q.limit(limit).all() items = database_cmd.GetModelItems().run(child_model, {x[0] for x in item_ids}) item_list = message.List(db.model_name(child_model), child_msg) [item_list.append(child_msg(x)) for x in items] return item_list
def update_metatags(item_type: enums.ItemType = enums.ItemType.Gallery, item_id: int = 0, metatags: dict = {}): """ Update metatags for an item Args: item_type: possible items are :py:attr:`.ItemType.Gallery`, :py:attr:`.ItemType.Page`, :py:attr:`.ItemType.Artist`, :py:attr:`.ItemType.Collection` item_id: id of item metatag: a dict of ``{ metatag_name : bool }`` Returns: bool indicating whether metatags were updated """ if not item_id: raise exceptions.APIError(utils.this_function(), "item_id must be a valid item id") item_type = enums.ItemType.get(item_type) _, db_item = item_type._msg_and_model( (enums.ItemType.Gallery, enums.ItemType.Collection, enums.ItemType.Page, enums.ItemType.Artist)) t = database_cmd.GetModelItems().run(db_item, {item_id}) if not t: raise exceptions.DatabaseItemNotFoundError( utils.this_function(), "{} with item id '{}' not found".format(item_type, item_id)) t = t[0] mtags = {} anames = db.MetaTag.all_names() for m, v in metatags.items(): if m not in anames: raise exceptions.APIError(utils.this_function(), f"Metatag name '{m}' does not exist") mtags[m] = v st = True if t: t.update("metatags", mtags) db.object_session(t).commit() else: st = False return message.Identity('status', st)
def get_common_tags(item_type: enums.ItemType = enums.ItemType.Collection, item_id: int = 0, limit: int = 10): """ Get the most common tags for item Args: item_type: possible items are :attr:`.ItemType.Artist`, :attr:`.ItemType.Grouping`, :attr:`.ItemType.Collection` item_id: id of item to fetch tags for limit: limit amount of tags returned Returns: .. code-block:: guess { namespace : [ tag message object, ...], ... } """ if not item_id: raise exceptions.APIError(utils.this_function(), "item_id must be a valid item id") item_type = enums.ItemType.get(item_type) _, db_item = item_type._msg_and_model( (enums.ItemType.Artist, enums.ItemType.Collection, enums.ItemType.Grouping)) nstags = database_cmd.MostCommonTags().run(db_item, item_id, limit) msg = _contruct_tags_msg(nstags) return message.Identity('tags', msg)
def set_config(cfg: dict): """ Set/update configuration Args: cfg: a dict containing ``namespace.key``:``value`` Returns: Status """ client_cfg = utils.get_context()['config'] for set_key in cfg: ns, key = _get_cfg(set_key, False) default_ns = ns.lower() in config.ConfigNode.default_namespaces if default_ns: t = config.ConfigNode.get_type(ns, key) if not isinstance(cfg[set_key], t): raise exceptions.APIError( utils.this_function(), "Setting '{}' expected '{}' but got '{}'".format( set_key, t, type(cfg[set_key]))) if config.ConfigNode.get_isolation_level( ns, key) == config.ConfigIsolation.client: client_cfg.setdefault(config.config.format_namespace(ns), {})[key.lower()] = cfg[set_key] continue with config.config.namespace(ns): config.config.update(key, cfg[set_key], create=not default_ns) return message.Message("updated")
def validate_child(self, key, child): # can't add to myself if child == self: raise exceptions.DatabaseError( utils.this_function(), "Cannot make NamespaceTag itself's child") return child
def get_related_count(item_type: enums.ItemType = enums.ItemType.Gallery, item_id: int = 0, related_type: enums.ItemType = enums.ItemType.Page): """ Get count of items related to given item Args: item_type: parent item item_id: id of parent item related_type: child item Returns: ``` { 'id': int, 'count': int } ``` """ item_type = enums.ItemType.get(item_type) related_type = enums.ItemType.get(related_type) _, parent_model = item_type._msg_and_model() child_msg, child_model = related_type._msg_and_model() col = db.relationship_column(parent_model, child_model) if not col: raise exceptions.APIError( utils.this_function(), "{} has no relationship with {}".format(related_type, item_type)) s = constants.db_session() count = s.query( child_model.id).join(col).filter(parent_model.id == item_id).count() return message.Identity('count', {'id': item_id, 'count': count})
def get(cls, key): # for some ungodly reason this check wouldnt work when calling from the client # so i ended comparing strings instead if repr(type(key)) == repr(cls): return key try: return cls[key] except KeyError: pass try: return cls(key) except ValueError: pass if isinstance(key, str): low_key = key.lower() for name, member in cls.__members__.items(): if name.lower() == low_key: return member raise exceptions.EnumError( utils.this_function(), "{}: enum member doesn't exist '{}'".format( cls.__name__, repr(key)))
def validate_aliases(self, key, alias): # can't add to myself if alias == self: raise exceptions.DatabaseError( utils.this_function(), "Cannot make NamespaceTag itself's alias") return alias
def scan_galleries(path: str, scan_options: dict = {}): """ Scan for galleries in the given directory/archive Args: path: path to directory/archive that exists on this system scan_options: options to apply to the scanning process, see :ref:`Settings` for available scanning options Returns: .. code-block:: guess { 'command_id': int, 'view_id': int } |async command| |temp view| """ path = io_cmd.CoreFS(path) if not path.exists: raise exceptions.CoreError( utils.this_function(), f"Path does not exists on this system: '{path.path}'") view_id = next(constants.general_counter) cmd_id = gallery_cmd.ScanGallery(services.AsyncService.generic).run( path, scan_options, view_id=view_id) return message.Identity('data', {'command_id': cmd_id, 'view_id': view_id})
def __init__(self, fpath): self._archive = None self._path = pathlib.Path(fpath) self._ext = self._path.suffix.lower() if not self._path.exists(): raise exceptions.ArchiveError( "Archive file does not exist. File '{}' not found.".format( str(self._path))) if not self._path.suffix.lower() in CoreFS.archive_formats(): raise exceptions.UnsupportedArchiveError(str(self._path)) try: with self._init.call_capture(self._ext, self._path) as plg: self._archive = plg.first_or_none() if not self._archive: raise exceptions.CoreError( utils.this_function(), "No valid archive handler found for this archive type: '{}'" .format(self._ext)) with self._test_corrupt.call_capture(self._ext, self._archive) as plg: r = plg.first_or_none() if r is not None: if r: raise exceptions.BadArchiveError(str(self._path)) with self._path_sep.call_capture(self._ext, self._archive) as plg: p = plg.first_or_none() self.path_separator = p if p else '/' except: if self._archive: self.close() raise
def validate_aliases(self, key, alias): # can't add to myself if alias == self: raise exceptions.DatabaseError( utils.this_function(), "Cannot make {} itself's alias".format( self.__class__.__name__)) return alias
def handshake(self, data=None): """ Sends a welcome message """ assert data is None or isinstance(data, dict) if isinstance(data, dict): log.d("Incoming handshake from client", self._address) data = data.get("data") if not constants.allow_guests: log.d("Guests are not allowed") self._check_both(utils.this_function(), "JSON dict", ('user', 'password'), data) u = p = None if isinstance(data, dict): u = data.pop('user', None) p = data.pop('password', None) self.get_context(u, p) self.session = Session() self.contexts[self.session.id] = self.context self.send( message.finalize("Authenticated", session_id=self.session.id)) else: log.d("Handshaking client:", self._address) msg = dict( version=meta.get_version().data(), guest_allowed=constants.allow_guests, ) self.send(message.finalize(msg))
def search_tags( search_query: str = "", search_options: dict = {}, only_namespace: bool = False, only_tag: bool = False, sort_by: enums.ItemSort = None, sort_desc: bool = False, limit: int = 100, offset: int = None, ): """ Search for tags Args: search_query: search string search_options: options to apply when filtering, see :ref:`Settings` for available search options only_namespace: only search for matching namespace <not implemented yet> only_tag: only search for matching tag <not implemented yet> sort_by: either a :py:class:`.ItemSort` or a sort index sort_desc: order descending (default is ascending) limit: limit the amount of items returned offset: offset the results by n items Returns: .. code-block:: guess { namespace : [ tag message object, ...], ... } """ if search_options: search_option_names = [ x.name for x in search_cmd._get_search_options() ] for n in search_options: if n not in search_option_names: raise exceptions.APIError( utils.this_function(), "Invalid search option name '{}'".format(n)) db_model = db.NamespaceTags model_ids = search_cmd.ModelFilter().run(db_model, search_query, search_options) order_exp, group_exp, join_exp = helpers._sort_helper( sort_by, sort_desc, db_model) items = database_cmd.GetModelItems().run(db_model, model_ids, limit=limit, offset=offset, join=join_exp, order_by=order_exp, group_by=group_exp) msg = _contruct_tags_msg(items) return message.Identity('tags', msg)
def get_context(self, user=None, password=None): "Creates or retrieves existing context object for this client" s = constants.db_session() user_obj = None if user or password: log.d("Client provided credentials, authenticating...") user_obj = s.query( db.User).filter(db.User.name == user).one_or_none() if user_obj: if not user_obj.password == password: raise exceptions.AuthError(utils.this_function(), "Wrong credentials") else: raise exceptions.AuthError(utils.this_function(), "Wrong credentials") else: log.d("Client did not provide credentials") if not constants.disable_default_user: log.d("Authenticating with default user") user_obj = s.query(db.User).filter( db.User.role == db.User.Role.default).one() else: if not constants.allow_guests: log.d("Guests are disallowed on this server") raise exceptions.AuthRequiredError(utils.this_function()) log.d("Authencticating as guest") user_obj = s.query(db.User).filter( db.and_op( db.User.address == self._ip, db.User.role == db.User.Role.guest)).one_or_none() if not user_obj: user_obj = db.User(role=db.User.Role.guest) s.add(user_obj) self.context = user_obj self.context.address = self._ip if not self.context.context_id: self.context.context_id = uuid.uuid4().hex self.context.config = None log.d("Client accepted") self._accepted = True s.commit()
def _command_msg(ids): if ids is None: return for x in ids: c = AsyncService.get_command(x) if not c: raise exceptions.CommandError( utils.this_function(), "Command with ID '{}' does not exist".format(x))
def _msg_and_model(item_type, allowed=tuple(), error=True): """ Get the equivalent Message and Database object classes for ItemType member Args: allowed: a tuple of ItemType members which are allowed, empty tuple for all members error: raise error if equivalent is not found, else return generic message object class """ if allowed and repr(item_type) not in (repr(x) for x in allowed): raise exceptions.APIError( utils.this_function(), "ItemType must be on of {} not '{}'".format( allowed, repr(item_type))) db_model = None try: db_model = getattr(db, item_type.name) except AttributeError: if error: raise exceptions.CoreError( utils.this_function(), "Equivalent database object class for {} was not found". format(item_type)) obj = None try: obj = getattr(message, item_type.name) except AttributeError: try: if db_model and issubclass(db_model, db.NameMixin): obj = getattr(message, db.NameMixin.__name__) except AttributeError: pass if not obj: if error: raise exceptions.CoreError( utils.this_function(), "Equivalent Message object class for {} was not found". format(item_type)) obj = message.DatabaseMessage return obj, db_model
def _get_cfg(keyname, ctx): try: ns, key = [x.strip() for x in keyname.strip().split('.') if x] except ValueError: raise exceptions.APIError( utils.this_function(), "Invalid setting keyname: '{}'".format(keyname)) cfg = constants.config if ns.lower() == 'this': pass if not cfg.key_exists(ns, key): raise exceptions.SettingsError( utils.this_function(), "Setting doesn't exist: '{}'".format(keyname)) return ns, key, cfg
def communicate(self, msg): """Send and receive data with server params: msg -- dict returns: dict from server """ if not self._accepted: raise exceptions.AuthError(utils.this_function(), "") self._send(bytes(json.dumps(msg), 'utf-8')) return self._recv()
def wrapper(func): if capture: self.default_capture_handlers.append(func) return func if self.default_handler: raise exceptions.CommandError( utils.this_function(), "Command '{}' has already been assigned a default handler". format(self.name)) self.default_handler = func return func
def get_tags(item_type: enums.ItemType = enums.ItemType.Gallery, item_id: int = 0, raw: bool = False): """ Get tags for item Args: item_type: possible items are :attr:`.ItemType.Gallery`, :attr:`.ItemType.Page`, :attr:`.ItemType.Grouping`, :attr:`.ItemType.Collection` item_id: id of item to fetch tags for raw: if true, tags from descendant ItemType's will not be included (this only makes sense when ItemType is :attr:`.ItemType.Gallery`) Returns: .. code-block:: guess { namespace : [ tag message object, ...], ... } """ if not item_id: raise exceptions.APIError(utils.this_function(), "item_id must be a valid item id") item_type = enums.ItemType.get(item_type) _, db_item = item_type._msg_and_model( (enums.ItemType.Gallery, enums.ItemType.Collection, enums.ItemType.Grouping, enums.ItemType.Page)) db_obj = database_cmd.GetModelItems().run(db_item, {item_id}) if db_obj: db_obj = db_obj[0] nstags = [] if db_obj: g_objs = [] if issubclass(db_item, db.TaggableMixin): nstags = db_obj.tags.all() if not raw and isinstance(db_obj, db.Gallery): g_objs.append(db_obj) else: for g in db_obj.galleries.all(): nstags.extend(g.tags.all()) if not raw: g_objs.append(g) for g_obj in g_objs: for p in g_obj.pages.all(): # TODO: we only need tags nstags.extend(p.tags.all()) msg = _contruct_tags_msg(nstags) return message.Identity('tags', msg)
def add_to_filter(gallery_id: int = 0, item_id: int = 0, item: dict = {}): """ Add a gallery to a galleryfilter Args: gallery_id: id of gallery item_id: id of existing galleryfilter, mutually exclusive with ``item`` parameter item: filter message object, mutually exclusive with ``item_id`` parameter Returns: bool whether gallery was added to filter or not """ if not gallery_id: raise exceptions.APIError(utils.this_function(), "gallery_id must be a valid gallery id") g = database_cmd.GetModelItems().run(db.Gallery, {gallery_id}) if not g: raise exceptions.DatabaseItemNotFoundError( utils.this_function(), "'{}' with id '{}' was not found".format( enums.ItemType.Gallery.name, gallery_id)) g = g[0] if item_id: p = database_cmd.GetModelItems().run(db.GalleryFilter, {item_id}) if not p: raise exceptions.DatabaseItemNotFoundError( utils.this_function(), "'{}' with id '{}' was not found".format( enums.ItemType.GalleryFilter.name, item_id)) p = p[0] elif item: p = message.GalleryFilter.from_json(item) g.filters.append(p) s = constants.db_session() s.add(g) s.commit() return message.Identity("status", True)
def get_settings(settings: list = [], ctx=None): """ Set settings Send empty list to get all key:values Args: set_list: a list of setting keys Returns: ``` { 'key.name': value } ``` """ utils.require_context(ctx) values = {} if settings: for set_key in settings: try: ns, key = [x.strip() for x in set_key.strip().split('.') if x] except ValueError: raise exceptions.APIError( utils.this_function(), "Invalid setting: '{}'".format(set_key)) if constants.config.key_exists(ns, key): values[set_key] = constants.config.get(ns, key) elif ns.lower() == 'self' and ctx.config and ctx.config.key_exists( ns, key): values[set_key] = ctx.config.get(ns, key) raise NotImplementedError else: raise exceptions.APIError( utils.this_function(), "Setting doesn't exist: '{}'".format(set_key)) else: raise NotImplementedError return message.Identity('settings', values)
def handshake(self, data={}, user=None, password=None, ignore_err=False): "Shake hands with server" if self.alive(): if user: self._last_user = user self._last_pass = password if not ignore_err and data: serv_error = data.get('error') if serv_error: if serv_error[ 'code'] == exceptions.AuthWrongCredentialsError.code: raise exceptions.AuthWrongCredentialsError( utils.this_function(), serv_error['msg']) elif serv_error[ 'code'] == exceptions.AuthRequiredError.code: raise exceptions.AuthRequiredError( utils.this_function(), serv_error['msg']) elif serv_error[ 'code'] == exceptions.AuthMissingCredentials.code: raise exceptions.AuthMissingCredentials( utils.this_function(), serv_error['msg']) else: raise exceptions.AuthError( utils.this_function(), "{}: {}".format(serv_error['code'], serv_error['msg'])) if not data: d = {} if user: d['user'] = user d['password'] = password self._send(message.finalize(d, name=self.name)) self.handshake(self._recv(), ignore_err=ignore_err) elif data: serv_data = data.get('data') if serv_data == "Authenticated": self.session = data.get('session') self._accepted = True