def add_object_at(self, object_brain_uid, row, column): """Adds an object to the specified position. If an object already exists at the given position, return False. Otherwise, return True """ if not self.can_add_object(object_brain_uid, row, column): return False uid = api.get_uid(object_brain_uid) obj = api.get_object(object_brain_uid) # If the object does not implement StorageLayoutContainer, then we # assume the object is not a container, rather the content that needs to # be contained (e.g. a Sample), so we set capacity and utilization to 1 samples_capacity = 1 samples_utilization = 1 if IStorageLayoutContainer.providedBy(obj): # This is a container, so infer the capacity and utilization samples_capacity = obj.get_samples_capacity() samples_utilization = obj.get_samples_utilization() row = api.to_int(row) column = api.to_int(column) layout = [{'uid': uid, 'row': row, 'column': column, 'samples_capacity': samples_capacity, 'samples_utilization': samples_utilization,}] for item in self.getPositionsLayout(): if item["row"] == row and item["column"] == column: continue layout.append(item.copy()) self.setPositionsLayout(layout) self.notify_parent() return True
def get_minimum_size(self): """Returns a tuple (rows, columns) that represents the minimum size this container can have without removing any of the objects it contains """ els = filter(self.is_taken, self.getPositionsLayout()) rows = map(lambda el: api.to_int(el['row']), els) or [0] cols = map(lambda el: api.to_int(el['column']), els) or [0] return (max(rows) + 1, max(cols) + 1)
def get_item_at(self, row, column): """Returns the layout item this container contains at the given position """ if not self.is_valid_position(row, column): return None for item in self.getPositionsLayout(): if api.to_int(item["row"]) == api.to_int(row): if api.to_int(item["column"]) == api.to_int(column): return item return None
def is_valid_position(self, row, column): """Returns whether the position defined is valid or not for this container's layout """ col_num = api.to_int(column, default=-1) if col_num < 0 or col_num >= self.getColumns(): return False row_num = api.to_int(row, default=-1) if row_num < 0 or row_num >= self.getRows(): return False return True
def get_object_position(self, object_brain_uid): """Returns the position as a tuple (row, column) of the object. If the object is not found, returns None """ uid = api.get_uid(object_brain_uid) if not uid: return None els = filter(lambda el: el.get("uid", "") == uid, self.getPositionsLayout()) if not els: return None return (api.to_int(els[0]['row']), api.to_int(els[0]['column']))
def pop(self, consumer_id): """Returns the next task to process, if any :param consumer_id: id of the consumer thread that will process the task :return: the task to be processed or None :rtype: queue.QueueTask """ with self.__lock: # Get the queued tasks queued = filter(lambda t: t.status == "queued", self._tasks) if not queued: # Maybe some tasks got stuck self._purge() consumer_tasks = self._get_consumer_tasks(consumer_id) if consumer_tasks: # This consumer has tasks running already, mark those that have # been running for more than 10s as failed. We assume here the # consumer always checks there is no thread running from his # side before doing a pop(). Thus, we consider that when a # consumer does a pop(), he did not succeed with running ones. for consumer_task in consumer_tasks: started = consumer_task.get("started") if not started or started + 10 < time.time(): msg = "Purged on pop ({})".format(consumer_id) self._fail(consumer_task, error_message=msg) return None if self.is_busy(): # We've reached the max number of tasks to process at same time return None # Get the names and paths of tasks that are currently running running_names = self.get_running_task_names() running_paths = self.get_running_context_paths() # Tasks are sorted from highest to lowest priority for task in queued: # Wait some secs before a task is available for pop. We do not # want to start processing the task while the life-cycle of the # request that added the task is still alive delay = capi.to_int(task.get("delay"), default=0) if task.created + delay > time.time(): continue # Be sure there is no other consumer working in a same type of # task and for the same path if task.name in running_names: # There is another consumer processing a task of same type if self.strip_path(task.context_path) in running_paths: # Same path, skip continue # Update and return the task task.update({ "started": time.time(), "status": "running", "consumer_id": consumer_id, }) return copy.deepcopy(task) return None
def __call__(self): protect.CheckAuthenticator(self.request.form) self.portal = api.get_portal() self.request.set('disable_plone.rightcolumn', 1) self.request.set('disable_border', 1) # Handle form submit form = self.request.form submitted = form.get("submitted", False) # nothing to do here if not submitted: return self.template() # Handle "Seed" action if form.get("seed", False): seeds = form.get("seeds", {}) for key, value in seeds.items(): value = api.to_int(value, None) message = "" if value is None: message = _( "Could not convert '{}' to an integer".format(value)) elif value == 0: del self.storage[key] message = _("Removed key {} from storage".format(key)) else: self.set_seed(key, value) message = _("Seeding key {} to {}".format(key, value)) self.add_status_message(message, "info") return self.template()
def get_max_retries(default=3): """Returns the number of retries before considering a task as failed """ registry_id = "senaite.queue.max_retries" max_retries = api.get_registry_record(registry_id) max_retries = api.to_int(max_retries, default=default) return max_retries >= 1 and max_retries or default
def get_max_seconds(default=120): """Returns the max number of seconds to wait for a task to finish """ registry_id = "senaite.queue.max_seconds_unlock" max_seconds = api.get_registry_record(registry_id) max_seconds = api.to_int(max_seconds, default=default) return max_seconds >= 30 and max_seconds or default
def get_min_seconds(default=3): """Returns the minimum number of seconds to book per task """ registry_id = "senaite.queue.min_seconds_task" min_seconds = api.get_registry_record(registry_id) min_seconds = api.to_int(min_seconds, default=default) return min_seconds >= 1 and min_seconds or default
def to_decimal(alpha_number, alphabet=ALPHABET, default=_marker): """Converts an alphanumeric code (e.g AB12) to an integer :param alpha_number: representation of an alphanumeric code :param alphabet: alphabet to use when alpha_number is a non-int string :type number: int, string, Alphanumber, float :type alphabet: string """ num = api.to_int(alpha_number, default=None) if num is not None: return num alpha_number = str(alpha_number) regex = re.compile(r"([A-Z]+)(\d+)", re.IGNORECASE) matches = re.findall(regex, alpha_number) if not matches: if default is not _marker: return default raise ValueError("Not a valid alpha number: {}".format(alpha_number)) alpha = matches[0][0] number = int(matches[0][1]) max_num = 10**len(matches[0][1]) - 1 len_alphabet = len(alphabet) for pos_char, alpha_char in enumerate(reversed(alpha)): index_char = alphabet.find(alpha_char) number += (index_char * max_num * len_alphabet**pos_char) return number
def resolve_sorting(self, query): """Resolves the sorting criteria for the given query """ sorting = {} # Sort on sort_on = query.get("sidx", None) sort_on = sort_on or query.get("sort_on", None) sort_on = sort_on == "Title" and "sortable_title" or sort_on if sort_on: sorting["sort_on"] = sort_on # Sort order sort_order = query.get("sord", None) sort_order = sort_order or query.get("sort_order", None) if sort_order in ["desc", "reverse", "rev", "descending"]: sorting["sort_order"] = "descending" else: sorting["sort_order"] = "ascending" # Sort limit sort_limit = query.get("sort_limit", query.get("limit")) sort_limit = api.to_int(sort_limit, default=30) if sort_limit: sorting["sort_limit"] = sort_limit return sorting
def __init__(self, name, request, context, *arg, **kw): super(QueueTask, self).__init__(*arg, **kw) if api.is_uid(context): context_uid = context context_path = kw.get("context_path") if not context_path: raise ValueError("context_path is missing") elif api.is_object(context): context_uid = api.get_uid(context) context_path = api.get_path(context) else: raise TypeError("No valid context object") # Set defaults kw = kw or {} task_uid = str(kw.get("task_uid", tmpID())) uids = map(str, kw.get("uids", [])) created = api.to_float(kw.get("created"), default=time.time()) status = kw.get("status", None) min_sec = api.to_int(kw.get("min_seconds"), default=get_min_seconds()) max_sec = api.to_int(kw.get("max_seconds"), default=get_max_seconds()) priority = api.to_int(kw.get("priority"), default=10) retries = api.to_int(kw.get("retries"), default=get_max_retries()) unique = self._is_true(kw.get("unique", False)) chunks = api.to_int(kw.get("chunk_size"), default=get_chunk_size(name)) username = kw.get("username", self._get_authenticated_user(request)) err_message = kw.get("error_message", None) self.update({ "task_uid": task_uid, "name": name, "context_uid": context_uid, "context_path": context_path, "uids": uids, "created": created, "status": status and str(status) or None, "error_message": err_message and str(err_message) or None, "min_seconds": min_sec, "max_seconds": max_sec, "priority": priority, "retries": retries, "unique": unique, "chunk_size": chunks, "username": str(username), })
def alpha_to_position(self, alpha): """Converts an alphanumeric value to a position """ alphabet = string.ascii_uppercase regex = re.compile(r"([A-Z]+)(\d+)", re.IGNORECASE) matches = re.findall(regex, alpha) alpha_part = matches[0][0] column = api.to_int(matches[0][1]) - 1 row = 0 mapping = map(lambda val: alphabet.index(val), alpha_part) for idx in range(len(mapping)): row += idx * len(alphabet) + mapping[idx] return (row, column)
def process(self, task): """Transition the objects from the task """ # The worksheet is the context worksheet = self.context uids = task.get("uids", []) slots = task.get("slots", []) # Sanitize the slots list and pad with empties slots = map(lambda s: _api.to_int(s, None) or "", slots) slots += [""] * abs(len(uids) - len(slots)) # Sort analyses so those with an assigned slot are added first # Note numeric values get precedence over strings, empty strings here uids_slots = zip(uids, slots) uids_slots = sorted(uids_slots, key=lambda i: i[1]) # Remove those with no valid uids uids_slots = filter(lambda us: _api.is_uid(us[0]), uids_slots) # Remove duplicate uids while keeping the order seen = set() uids_slots = filter(lambda us: not (us[0] in seen or seen.add(us[0])), uids_slots) # Remove uids that are already in the worksheet (just in case) layout = filter(None, worksheet.getLayout() or []) existing = map(lambda r: r.get("analysis_uid"), layout) uids_slots = filter(lambda us: us[0] not in existing, uids_slots) # If there are too many objects to process, split them in chunks to # prevent the task to take too much time to complete chunks = get_chunks_for(task, items=uids_slots) # Process the first chunk for uid, slot in chunks[0]: # Add the analysis slot = slot or None analysis = _api.get_object_by_uid(uid) worksheet.addAnalysis(analysis, slot) # Reindex the worksheet worksheet.reindexObject() if chunks[1]: # Unpack the remaining analyses-slots and add them to the queue uids, slots = zip(*chunks[1]) api.add_assign_task(worksheet, analyses=uids, slots=slots)
def get_chunk_size(name_or_action=None): """Returns the number of items to process at once for the given task name :param name_or_action: task name or workflow action id :returns: the number of items from the task to process async at once :rtype: int """ chunk_size = api.get_registry_record("senaite.queue.default") chunk_size = api.to_int(chunk_size, 0) if chunk_size <= 0: # Queue disabled return 0 if name_or_action: # TODO Retrieve task-specific chunk-sizes via adapters pass if chunk_size < 0: chunk_size = 0 return chunk_size
def minimum_length(self): """Minimum required length of the search term """ min_length = self.request.get("minLength", 0) return api.to_int(min_length, 0)
def set_seed(self, key, value): """Set a number of the number generator """ number_generator = getUtility(INumberGenerator) return number_generator.set_number(key, api.to_int(value))
def get_layout_subfield_sum(self, subfield): """Returns the sum of the elements stored in the layout for the subfield name passed in. If the value for the element is not floatable, uses 0 """ layout = self.getPositionsLayout() return sum(map(lambda el: api.to_int(el[subfield], default=0), layout))
def extract_period(val, period): num = re.findall(r'(\d{1,2})' + period, val) or [0] return api.to_int(num[0], default=0)
def num_page(self): """Returns the number of page to render """ return api.to_int(self.request.get("page", None), default=1)
def num_rows_page(self): """Returns the number of rows per page to render """ return api.to_int(self.request.get("rows", None), default=10)
def get_default_num_samples(): """Returns the num of Samples (Columns) to be displayed in Sample Add Form """ ar_count = api.get_setup().getDefaultNumberOfARsToAdd() return api.to_int(ar_count, 1)