def add_tasks_todoist(project_name: str, tasks: List[Task]): # Authenticate with the todoist API api_token = open("todoist_token.txt").read() api = TodoistAPI(api_token) api.sync() # Find the uni project or create it projects = api.state["projects"] projects = list(filter(lambda p: p["name"] == project_name, projects)) if len(projects) != 0: project_id = projects[0]["id"] else: procject = api.projects.add(project_name) api.commit() project_id = procject["id"] # Add the tasks for task in tasks: api.items.add( task.text, project_id=project_id, due={"date": task.duedate.strftime("%Y-%m-%d")}, ) print(task) api.commit()
class Todoist: def __init__(self): self.token = token self.api = TodoistAPI(self.token) self.api.sync() def sync(self): self.api.sync() def get_obj(self, obj): if obj == 'state': return self.api.state elif obj == 'projects': return self.api.state['projects'] elif obj == 'labels': return self.api.state['labels'] elif obj == 'items': return self.api.state['items'] elif obj == 'sections': return self.api.state['sections'] else: return 'Object Not Found' def get_items(self, p_id=False): all_items = self.get_obj('items') if p_id: select_items = [] for item in all_items: if item['project_id'] == p_id: select_items.append(item) all_items = select_items return all_items
def test_add_indox_task(): g.db.close() content = 'Это новая задача!' debug_processing('{"type": "message_new", ' '"object": {"id": 43, "date": 1492522323, "out": 0, ' '"user_id": 481116745, "read_state": 0, ' '"body": "' + content + '"}}') acc = Subscription.get(Subscription.messenger_user_id == 481116745).account service = TodoistService() api = TodoistAPI(AccessToken.get(account=acc.id).token) api.sync() found_content = [ item['content'] for item in api.items.all() if item['content'] == content and item['project_id'] == service.project_name_to_id(api=api, proj_name='Inbox') ][0] assertEqual(funcname=test_add_indox_task.__name__, a=found_content, b=content) # assertEqual(vkapi.sended_message, 'Я не понял команды. Попробуйте еще раз.', __name__) service.delete_task(task=content, proj='Inbox', user_id=481116745)
class TaskManager: def __init__(self, settings): self.todoist = TodoistAPI(settings['API_KEY']) self.todoist.sync() self.user = self.todoist.state['user']['id'] self.labels = convert_labels(self.todoist.state['labels']) def _sync(func): def synced(self, *args, **kwargs): self.todoist.sync() return func(self, *args, **kwargs) return synced @_sync def avg_spoons(self): completed_spoonsy_tasks = [ task for task in self.get_tasks() if is_spoonsy_task(task, self.labels) and task['date_completed'] ] return mean([ calculate_values(task['labels']) for task in completed_spoonsy_tasks ]) @_sync def get_projects(self): return [ project for project in self.todoist.state['projects'] if project_owned_by_user(project, self.user) ] @_sync def get_tasks(self, project_name='all'): all_tasks = self.todoist.state['items'] if project_name == 'all': return all_tasks else: project = get_project_or_none(all_tasks, project_name) return [ item for item in all_tasks if item['project_id'] == project['id'] ]
def initiate_api(access_token): """Initiate and sync Todoist API""" api = TodoistAPI(access_token) api.sync() if bool(api['user']): return api else: return None
def add_task(task, project='', date_string='', accuracy='day', due_datetime=''): g.db.close() import time time.sleep(1) body = '{0}{1}{2}.'.format(task, ' в ' + project if project != '' else '', '. ' + date_string if datetime != '' else '') debug_processing('{"type": "message_new", ' '"object": {"id": 43, "date": 1492522323, "out": 0, ' '"user_id": 481116745, "read_state": 0, ' '"body": "' + body + '"}}') time.sleep(1) if 'Все верно?' in vkapi.sended_message: # bad debug_processing('{"type": "message_new", ' '"object": {"id": 43, "date": 1492522323, "out": 0, ' '"user_id": 481116745, "read_state": 0, ' '"body": "Ага!"}}') time.sleep(1) acc = Subscription.get(Subscription.messenger_user_id == 481116745).account service = TodoistService() api = TodoistAPI(AccessToken.get(account=acc.id).token) api.sync() project = 'Inbox' if project == '' else project try: found_content = [ item for item in api.items.all() if item['content'] == task + '.' and item['project_id'] == service.project_name_to_id(api=api, proj_name=project) ][0] except Exception as e: print(api.items.all()) found_content = [ item for item in api.items.all() if item['content'] == task + '.' and item['project_id'] == service.project_name_to_id(api=api, proj_name=project) ][0] assertEqual(funcname=add_task.__name__, a=found_content.data['content'], b=task + '.') # import datetime as dt # timedelts = {'day': dt.timedelta(days=1), 'hour': dt.timedelta(hours=1)} # assertTrue(due_datetime < found_content['due_date_utc'] + timedelts[accuracy] # and due_datetime > found_content['due_date_utc'] - timedelts[accuracy], funcname=add_task.__name__) service.delete_task(task=task + '.', proj=project, user_id=481116745)
def todoist_reading_list(handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() reading_list = t_utils.get_project_by_name(todoist, 'reading list') categories = t_utils.get_child_projects(todoist, reading_list) for task in todoist.state['items']: for project in categories: if task['project_id'] == project['id']: content = task['content'] logging.info(content) m = re.search(r'\[([^\[]+)\]\((.*)\)', content) # The todoist app stores links as either a markdown formatted link or "title - url" # if markdown links fail, try to parse the "title - url" format. if m: logging.info("markdown group") title = m.group(1) url = m.group(2) logging.info(title) logging.info(url) else: logging.info("hyphen group") content_components = content.split(" - ") if len(content_components) > 1: title = ''.join(content_components[:-1]) url = content_components[-1].strip() logging.info(title) logging.info(url) else: task.update(content="FAILED TO PARSE: " + content) task.move(parent_id=reading_list['id']) comments = t_utils.get_comments_for_task(todoist, task) article = article_parser.parse_url(url) data = { 'url': url, 'title': title, 'summary': article.summary, 'keywords': article.keywords, 'text': article.text, 'published_date': article.publish_date, 'notes': comments, 'category': project['name'] } handler.publish('archive_article', url, data) task.complete() todoist.commit()
def openTodoist(): f = open('config.json',) config = json.load(f) f.close() if config['api-token']=='': print('Please add an API token to config.json') exit() api = TodoistAPI(token=config['api-token']) api.sync() return api
class TodoistWrapper: def __init__(self, conf): self.todoist = TodoistAPI(conf['secret']) self.conf = conf self.todoist.sync() def get_projects(self): projs = [Project(self, p['id']) for p in self.todoist['projects']] if not self.conf['show_inbox']: return [proj for proj in projs if proj.name != 'Inbox'] else: return projs def project_data(self, project_id): return self.todoist.projects.get_data(project_id) def task_data(self, task_id): return self.todoist.items.get(task_id) def create_task(self, name, project_id): self.todoist.items.add(name, project_id) self.todoist.commit() def create_project(self, project_name): self.todoist.projects.add(project_name) self.todoist.commit() def complete_task(self, task_id): try: self.todoist.items.complete([int(task_id)]) self.todoist.commit() except ValueError: raise CmdError("Argument must be a task id.") def _get_project_task_ids(self, project_id): try: project = Project(self, int(project_id)) task_ids = [task.obj_id for task in project] return task_ids except ValueError: raise CmdError("Argument must be a project id.") def complete_project(self, project_id): self.todoist.items.complete(self._get_project_task_ids(project_id)) self.todoist.commit() def clear_project(self, project_id): self.todoist.items.delete(self._get_project_task_ids(project_id)) self.todoist.commit() def delete_project(self, project_id): self.todoist.projects.delete([project_id]) self.todoist.commit()
def add_issue_to_todoist(event=None, handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() issue_project = t_utils.get_project_by_name(todoist, 'issues') r = event.data todoist.items.add( '{} [#{}]({})'.format(r['title'], r['number'], r['html_url']), project_id=issue_project['id'], ) # todo close todos for prs/issues that are no longer active todoist.commit()
def add_mention_to_todoist(event=None, handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() mention_project = t_utils.get_project_by_name(todoist, 'GH Mentions') r = event.data todoist.items.add( 'GH Mention - {} [#{}]({})'.format(r['title'], r['number'], r['url']), # project_id=mention_project['id'], # auto_reminder=True, # due={"string": "next workday at 9am"}, priority=4) # todo close todos for prs/issues that are no longer active todoist.commit()
class User(db.Model): id = db.Column(db.Integer, primary_key=True) tg_id = db.Column(db.BigInteger, unique=True) first_name = db.Column(db.String(255)) last_name = db.Column(db.String(255)) username = db.Column(db.String(100)) todoist_id = db.Column(db.BigInteger, nullable=True, index=True) state = db.Column(db.String(36), default='', index=True) auth = db.Column(db.String(255), default='') is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, nullable=False) last_active_at = db.Column(db.DateTime, nullable=True) # noinspection PyTypeChecker def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.update = None # type: Update self.message = None # type: Message self.api = None # type: TodoistAPI self.now = None # type: datetime def is_authorized(self): return bool(self.auth) def first_init_api(self): self.init_api() if not self.is_authorized(): return from app.telegram.handlers import bot bot().base_welcome(self) def init_api(self): if not self.is_authorized(): return try: with app.app_context(): self.api = TodoistAPI(self.auth, cache=app.config['TODOIST']['CACHE']) result = self.api.sync() if 'error' in result: raise SyncError if 'user' in result and not self.todoist_id: self.todoist_id = result['user']['id'] except SyncError: self.auth = '' self.todoist_id = None self.api = None def send_message(self, text, **kwargs): from app.telegram.handlers import bot try: return bot().send_message(self.tg_id, text, **kwargs) except Unauthorized: self.is_active = False db.session.add(self) db.session.commit() return False
def add_issue_to_todoist(event=None, handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() issue_project = None for p in todoist.state['projects']: if p['name'].lower() == 'issues': issue_project = p break r = event.data todoist.items.add( '{} [#{}]({})'.format(r.title, r.number , r.html_url), project_id=issue_project['id'], ) # todo close todos for prs/issues that are no longer active todoist.commit()
def add_pr_to_todoist(event=None, handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() review_project = None for p in todoist.state['projects']: if p['name'].lower() == 'review requests': review_project = p break r = event.data todoist.items.add('{} [{} #{}]({})'.format(r.title, r.base.repo.full_name, r.number, r.html_url), project_id=review_project['id'], auto_reminder=True, due={"string": "next workday at 9am"}, priority=4) # todo close todos for prs/issues that are no longer active todoist.commit()
class TodoistItems: def __init__(self, token): self.api_token = token self.api = TodoistAPI(self.api_token) self.api.sync() def key_check(self, item, key): try: result = item[key] return result except: return False def date_check(self, item, date): if self.key_check(item, 'due'): if item['due']['lang'] == 'ja': if date.replace('/', '-') in item['due']['date']: return True elif '毎日' in item['due']['string']: return True else: try: if date == parse(item['due']['date']).strftime('%Y/%m/%d'): return True except ValueError: pass return False def get_project_name(self, item): if self.key_check(item, 'project_id'): project_id = item['project_id'] else: project_id = item['parent_id'] project = self.api.projects.get_by_id(project_id) return project['name'] def find_by_date(self, date): return [ item for item in self.api['items'] if self.date_check(item, date) ]
def main(args=None): """Console script for greminders2todoist.""" flow = MyInstalledAppFlow.from_client_secrets_file('client.json', SCOPES) creds: Credentials = flow.run_local_server(port=3423) api = TodoistAPI(creds.token) api.sync() tree: _ElementTree = etree.parse(open('./Reminders.html'), etree.HTMLParser()) pprint(scan_fields(tree)) rows = [ dict(type='task', content=task.title, priority=4, indent=None, author=None, responsible=None, date=proc_date(task), date_lang='en', timezone=None) for task in gen_tasks(tree) if task.state != 'archived' and ( task.recurrence or task.due and task.due > datetime.now()) ] with open('out.csv', 'w') as outfile: writer = csv.DictWriter(outfile, [ 'type', 'content', 'priority', 'indent', 'author', 'responsible', 'date', 'date_lang', 'timezone' ]) writer.writeheader() writer.writerows(rows) [inbox] = [p for p in api.state['projects'] if p['name'] == 'Inbox'] for task in rows: api.items.add(task['content'], inbox['id'], date_string=task['date']) api.commit() return 0
def add_pr_to_todoist(event=None, handler=None): todoist = TodoistAPI(get_credential('todoist_token')) todoist.sync() # review_project = t_utils.get_project_by_name(todoist, 'review requests') r = event.data title = 'PR' if r['base']['repo']['full_name'] == 'pulp/pulp_ansible': title = 'Pulp Ansible PR' todoist.items.add( '{} - {} [{} #{}]({})'.format(title, r['title'], r['base']['repo']['full_name'], r['number'], r['html_url']), # project_id=review_project['id'], # auto_reminder=True, # due={"string": "next workday at 9am"}, # priority=4 ) # todo close todos for prs/issues that are no longer active todoist.commit()
def main(): with token() as t: todo = TodoistAPI(t) todo.sync() yield todo todo.commit()
class TodoistLibrary: api: TodoistAPI ### Test Setup ### def todoist_api_client_is_connected(self): self.api = TodoistAPI('313f6bf203b35e7ac56e39561a80633e459c9c54') self.api.sync() ### Test Teardown ### def test_clean_up(self): self.delete_project() self.api.commit() def commit_and_sync(self): self.api.commit() self.api.sync() def delete_project(self): for project in self.api.projects.all(): self.api.projects.delete(project["id"]) self.api.items.all().clear() def create_project_with_name(self, name: str = None): self.api.projects.add(name) def create_project_with_name_and_parent_id(self, name: str, parent_id: int = None): self.api.projects.add(name=name, parent_id=parent_id) def create_task_with_name(self, name_project: str, name_task: str): project = self.api.projects.add(name_project) self.api.items.add(name_task, project_id=project['id']) def create_task_with_name_and_due_date(self, name_project: str, name_task: str, due_date: str): project = self.api.projects.add(name_project) self.api.items.add(name_task, project_id=project['id'], due={'date': due_date}) def create_task_and_subtask_with_name(self, name_project: str, name_task: str, name_subtask: str): project = self.api.projects.add(name_project) task_id = self.api.items.add(name_task, project_id=project['id'], due={'date': '2020-07-18T07:00:00Z'}) subtask = self.api.items.add(name_subtask, project_id=project['id'], due={'date': '2020-07-18T05:00:00Z'}) subtask.move(parent_id=task_id['id']) def create_project_and_add_comment(self, name_project, name_task): project = self.api.projects.add(name_project) task_id = self.api.items.add(name_task, project_id=project['id'], due={'date': '2020-07-18T07:00:00Z'}) self.api.notes.add(task_id['id'], 'Comment3') def create_parent_project_and_child_project(self, parent: str, child: str): parent_project = self.api.projects.add(parent) self.api.projects.add(child, parent_id=parent_project["id"]) def create_task_with_name_and_priority(self, name_project, task_name, priority: int): self.api.projects.add(name_project) self.api.items.add(task_name, priority=priority) def assert_project_with_name_exists(self, name_project): projects = self.api.projects.all(filt=lambda project: project['name'] == name_project) assert len(projects) > 0, "Project could not be found" project = projects[0] assert project['name'] == name_project, "Project is not created" def assert_task_exists(self, name_task): name_tasks = self.api.items.all(filt=lambda task: task['content'] == name_task) assert len(name_tasks) > 0, "Parent task could not be found" task = name_tasks[0] assert task['content'] == name_task, "Task is not created" def assert_task_has_due_date(self, task_name, due_date): tasks = self.api.items.all(filt=lambda task: task["content"] == task_name) assert len(tasks) > 0, "Parent task could not be found" task = tasks[0] assert task['due']['date'] == due_date, "Task due date is wrong" def assert_task_with_subtask_exists(self, parent_task, child_task): parent_tasks = self.api.items.all(filt=lambda task: task["content"] == parent_task) assert len(parent_tasks) > 0, "Parent task could not be found" parent = parent_tasks[0] child_tasks = self.api.items.all(filt=lambda project: project["content"] == child_task) assert len( child_tasks) > 0, "Child task could not be found" child = child_tasks[0] assert child["parent_id"] == parent['id'], "Task is not parent of another project" def assert_project_is_parent_of_another_project(self, parent, child): projects = self.api.projects.all(filt=lambda project: project["name"] == parent) assert len(projects) > 0, "Parent project could not be found" parent_project = projects[0] projects = self.api.projects.all(filt=lambda project: project["name"] == child) assert len(projects) > 0, "Child project could not be found" child_project = projects[0] assert child_project["parent_id"] == parent_project['id'], "Project is not parent of another project" def commit_and_sync_expect_error(self, error: str, error_code: int = None): try: self.api.commit() self.api.sync() assert False, "Server did not throw any error" except AssertionError as ae: raise ae except SyncError as ex: assert ex.args[1]['error'] == error, "Wrong error message" if error_code is not None and ex.args[1]['error_code'] != error_code: assert False, "Wrong error code" def task_has_priority(self, task_name, priority: int): tasks = self.api.items.all(filt=lambda task: task["content"] == task_name) assert len(tasks) > 0, "Task could not be found" task = tasks[0] assert task['priority'] == priority, "Task priority is wrong"
import config import urwid from todoist import TodoistAPI import pprint token = config.todoist_api_key api = TodoistAPI(token) api.sync() items = api.state['items'] pprint.pprint(len(items)) for item in items: # pprint.pprint(item, indent=4) # print('++++++++++++++++++++++++++++++++++') pprint.pprint('Content: ' + item['content'], indent=4) pprint.pprint('Date Completed: ' + str(item['date_completed']), indent=4) pprint.pprint('Checked: ' + str(item['checked']), indent=4) # pprint.pprint('Due Date: ' + item['due']['date'], indent=4) print( '////////////////////////////////////////////////////////////////////////////////////\n' ) def exit_on_q(key): if key in ('q', 'Q'): raise urwid.ExitMainLoop() palette1 = [('tl_txt_pal', 'light magenta', 'black'), ('tr_txt_pal', 'light blue', 'black'),
class Todoist(SingletonMixin): """A wrapper for the Todoist API.""" def __init__(self, api_token: str) -> None: super().__init__() self.api = TodoistAPI(api_token) self.data: JsonDict = {} self.projects: DictProjectId = {} self._allow_creation = False def smart_sync(self): """Only perform a full resync if needed.""" if not self.data.get("projects", {}): # If internal data has no projects, reset the state and a full (slow) sync will be performed. self.api.reset_state() partial_data = {} for attempt in range(3): # For some reason, sometimes this sync() method returns an empty string instead of a dict. # In this case, let's try again for a few times until we get a dictionary. partial_data = self.api.sync() if isinstance(partial_data, dict): break LOGGER.warning(f"Retrying, attempt {attempt + 1}: partial_data is not a dict(): {partial_data!r}") self._merge_new_data(partial_data) self.projects = dict(PROJECTS_NAME_ID_JMEX.search(self.data)) def _merge_new_data(self, partial_data: JsonDict): if not self.data: self.data = partial_data return for key, value in partial_data.items(): if isinstance(value, list): if key not in self.data: self.data[key] = [] self.data[key].extend(value) elif isinstance(value, dict): if key not in self.data: self.data[key] = {} self.data[key].update(value) else: self.data[key] = value def keys(self): """Keys of the data.""" return sorted(self.data.keys()) @deprecated(reason="use find* functions instead") def fetch( self, element_name: str, return_field: str = None, filters: JsonDict = None, index: int = None, matching_function=all, ) -> List[Any]: """Fetch elements matching items that satisfy the desired parameters. :param element_name: Name of the element to search. E.g. 'projects', 'items'. :param return_field: Name of the return field. If None, return the whole element. :param filters: Parameters for the search. :param index: Desired index to be returned. If nothing was found, return None. :param matching_function: ``all`` items by default, but ``any`` can be used as well. """ if not filters: values_to_list: JsonDict = {} else: values_to_list = {key: [value] if not isinstance(value, list) else value for key, value in filters.items()} found_elements = [ element[return_field] if return_field else element for element in self.data[element_name] if not filters or matching_function(element[key] in value for key, value in values_to_list.items()) ] if index is not None: return found_elements[index] if found_elements else None return found_elements @deprecated(reason="use find* functions instead") def fetch_first(self, element_name: str, return_field: str = None, filters: JsonDict = None) -> Optional[Any]: """Fetch only the first result from the fetched list, or None if the list is empty.""" return self.fetch(element_name, return_field, filters, 0) def find_project_id(self, exact_name: str) -> Optional[int]: """Find a project ID by its exact name. :param exact_name: Exact name of a project. """ return self.projects.get(exact_name, None) def find_projects(self, partial_name: str = "") -> DictProjectId: """Find projects by partial name. :param partial_name: Partial name of a project. """ return { name: project_id for name, project_id in self.projects.items() if partial_name.casefold() in name.casefold() } def find_project_items(self, exact_project_name: str, extra_jmes_expression: str = "") -> List[JsonDict]: """Fetch all project items by the exact project name. :param exact_project_name: Exact name of a project. :param extra_jmes_expression: Extra JMESPath expression to filter fields, for instance. """ project_id = self.find_project_id(exact_project_name) if not project_id: return [] return jmespath.search(f"items[?project_id==`{project_id}`]{extra_jmes_expression}", self.data) def find_items_by_content(self, exact_project_name: str, partial_content: str) -> List[JsonDict]: """Return items of a project by partial content. :param exact_project_name: Exact name of a project. :param partial_content: Partial content of an item. """ clean_content = partial_content.casefold() return [ item for item in self.find_project_items(exact_project_name) if clean_content in item.get("content", "").casefold() ]
class ServiceTodoist(ServicesMgr): def __init__(self, token=None, **kwargs): super(ServiceTodoist, self).__init__(token, **kwargs) self.AUTH_URL = 'https://todoist.com/oauth/authorize' self.ACC_TOKEN = 'https://todoist.com/oauth/access_token' self.REQ_TOKEN = 'https://todoist.com/oauth/access_token' self.consumer_key = settings.TH_TODOIST['client_id'] self.consumer_secret = settings.TH_TODOIST['client_secret'] self.scope = 'task:add,data:read,data:read_write' self.service = 'ServiceTodoist' self.oauth = 'oauth2' if token: self.token = token self.todoist = TodoistAPI(token) def read_data(self, **kwargs): """ get the data from the service as the pocket service does not have any date in its API linked to the note, add the triggered date to the dict data thus the service will be triggered when data will be found :param kwargs: contain keyword args : trigger_id at least :type kwargs: dict :rtype: list """ trigger_id = kwargs.get('trigger_id') date_triggered = kwargs.get('date_triggered') data = [] project_name = 'Main Project' items = self.todoist.sync() for item in items.get('items'): date_added = arrow.get(item.get('date_added'), 'ddd DD MMM YYYY HH:mm:ss ZZ') if date_added > date_triggered: for project in items.get('projects'): if item.get('project_id') == project.get('id'): project_name = project.get('name') data.append({'title': "From TodoIst Project {0}" ":".format(project_name), 'content': item.get('content')}) cache.set('th_todoist_' + str(trigger_id), data) return data def save_data(self, trigger_id, **data): """ let's save the data :param trigger_id: trigger ID from which to save data :param data: the data to check to be used and save :type trigger_id: int :type data: dict :return: the status of the save statement :rtype: boolean """ kwargs = {} title, content = super(ServiceTodoist, self).save_data(trigger_id, data, **kwargs) if self.token: if title or content or \ (data.get('link') and len(data.get('link'))) > 0: content = title + ' ' + content + ' ' + data.get('link') self.todoist.add_item(content) sentence = str('todoist {} created').format(data.get('link')) logger.debug(sentence) status = True else: status = False else: logger.critical("no token or link provided for " "trigger ID {} ".format(trigger_id)) status = False return status
def _api_for_user(self, user_id): acc = Subscription.get( Subscription.messenger_user_id == user_id).account api = TodoistAPI(AccessToken.get(AccessToken.account == acc).token) api.sync() return api
class Todoist(SingletonMixin): """A wrapper for the Todoist API.""" def __init__(self, api_token: str) -> None: super().__init__() self.api = TodoistAPI(api_token) self.data: JsonDict = {} self.projects: DictProjectId = {} self._allow_creation = False def smart_sync(self): """Only perform a full resync if needed.""" if not self.data.get("projects", {}): # If internal data has no projects, reset the state and a full (slow) sync will be performed. self.api.reset_state() partial_data = {} for attempt in range(3): # For some reason, sometimes this sync() method returns an empty string instead of a dict. # In this case, let's try again for a few times until we get a dictionary. partial_data = self.api.sync() if isinstance(partial_data, dict): break LOGGER.warning( f"Retrying, attempt {attempt + 1}: partial_data is not a dict(): {partial_data!r}" ) self._merge_new_data(partial_data) self.projects = dict(PROJECTS_NAME_ID_JMEX.search(self.data)) def _merge_new_data(self, partial_data: JsonDict): if not self.data: self.data = partial_data return for key, value in partial_data.items(): if isinstance(value, list): if key not in self.data: self.data[key] = [] self.data[key].extend(value) elif isinstance(value, dict): if key not in self.data: self.data[key] = {} self.data[key].update(value) else: self.data[key] = value def keys(self): """Keys of the data.""" return sorted(self.data.keys()) @deprecated(reason="use find* functions instead") def fetch( self, element_name: str, return_field: str = None, filters: JsonDict = None, index: int = None, matching_function=all, ) -> List[Any]: """Fetch elements matching items that satisfy the desired parameters. :param element_name: Name of the element to search. E.g. 'projects', 'items'. :param return_field: Name of the return field. If None, return the whole element. :param filters: Parameters for the search. :param index: Desired index to be returned. If nothing was found, return None. :param matching_function: ``all`` items by default, but ``any`` can be used as well. """ if not filters: values_to_list: JsonDict = {} else: values_to_list = { key: [value] if not isinstance(value, list) else value for key, value in filters.items() } found_elements = [ element[return_field] if return_field else element for element in self.data[element_name] if not filters or matching_function( element[key] in value for key, value in values_to_list.items()) ] if index is not None: return found_elements[index] if found_elements else None return found_elements @deprecated(reason="use find* functions instead") def fetch_first(self, element_name: str, return_field: str = None, filters: JsonDict = None) -> Optional[Any]: """Fetch only the first result from the fetched list, or None if the list is empty.""" return self.fetch(element_name, return_field, filters, 0) def find_project_id(self, exact_name: str) -> Optional[int]: """Find a project ID by its exact name. :param exact_name: Exact name of a project. """ return self.projects.get(exact_name, None) def find_projects(self, partial_name: str = "") -> DictProjectId: """Find projects by partial name. :param partial_name: Partial name of a project. """ return { name: project_id for name, project_id in self.projects.items() if partial_name.casefold() in name.casefold() } def find_project_items(self, exact_project_name: str, extra_jmes_expression: str = "") -> List[JsonDict]: """Fetch all project items by the exact project name. :param exact_project_name: Exact name of a project. :param extra_jmes_expression: Extra JMESPath expression to filter fields, for instance. """ project_id = self.find_project_id(exact_project_name) if not project_id: return [] return jmespath.search( f"items[?project_id==`{project_id}`]{extra_jmes_expression}", self.data) def find_items_by_content(self, exact_project_name: str, partial_content: str) -> List[JsonDict]: """Return items of a project by partial content. :param exact_project_name: Exact name of a project. :param partial_content: Partial content of an item. """ clean_content = partial_content.casefold() return [ item for item in self.find_project_items(exact_project_name) if clean_content in item.get("content", "").casefold() ]