def freeze_tasks( repo: Repository, selector: TaskSelector, ) -> None: """Freeze a list of tasks based on a task filter.""" tasks = _tasks_from_selector(repo, selector) for task in tasks: if type(task) == Task: child_task = task if child_task.parent_id is None: raise ValueError( f"Task {child_task.id_}: {child_task.description} is not the child" " of any recurrent task, so it can't be frozen") parent_task = repo.get(child_task.parent_id, [RecurrentTask]) elif type(task) == RecurrentTask: parent_task = task try: child_task = repo.search( { "active": True, "parent_id": task.id_ }, [Task])[0] except EntityNotFoundError as error: raise EntityNotFoundError( f"The recurrent task {task.id_}: {task.description} has no active " "children") from error parent_task.freeze() repo.add(parent_task) repo.delete(child_task) log.info( f"Frozen recurrent task {parent_task.id_}: {parent_task.description} and " f"deleted it's last child {child_task.id_}") repo.commit()
def test_thaw_task_by_id( self, runner: CliRunner, insert_frozen_parent_task_e2e: RecurrentTask, repo_e2e: Repository, caplog: LogCaptureFixture, ) -> None: """ Given: A frozen recurrent task When: Thawed Then: The task is back active and it's next children is breed. """ parent_task = insert_frozen_parent_task_e2e now = datetime.now() result = runner.invoke(cli, ["thaw", str(parent_task.id_)]) parent_task = repo_e2e.get(parent_task.id_, [RecurrentTask]) child_task = repo_e2e.search({"parent_id": parent_task.id_}, [Task])[0] assert result.exit_code == 0 assert ( "pydo.services", logging.INFO, f"Thawed task {parent_task.id_}: {parent_task.description}, and created " f"it's next child task with id {child_task.id_}", ) in caplog.record_tuples assert parent_task.state == TaskState.BACKLOG assert parent_task.active is True assert child_task.state == TaskState.BACKLOG assert child_task.active is True assert child_task.due is not None assert child_task.due > now
def test_repository_search_raises_error_if_searching_by_inexistent_value( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """If no object has a value like the search criteria raise the desired error.""" entity = inserted_entities[0] with pytest.warns( UserWarning, match="From 2022-06-10.*return an empty list" ), pytest.raises( EntityNotFoundError, match=( f"There are no entities of type {entity.model_name} in the " "repository that match the search filter {'id_': 'inexistent_value'}" ), ): repo.search({"id_": "inexistent_value"}, type(entity)) # act
def print_task_report( repo: Repository, config: config.Config, report_name: str, task_selector: Optional[TaskSelector] = None, ) -> None: """Gather the common tasks required to print several tasks.""" if task_selector is None: task_selector = TaskSelector() # Initialize the Report ( columns, labels, default_task_filter, sort_criteria, ) = _get_task_report_configuration(config, report_name) colors = Colors(**config.data["themes"][config.get("theme")]) report = Report(labels=labels, colors=colors) # Complete the task_selector with the report task_filter task_selector.task_filter.update(default_task_filter) # Change the sorting of the report with the values of the task selector if task_selector.sort != []: sort_criteria = task_selector.sort with suppress(KeyError): if task_selector.task_filter["type"] == "recurrent_task": task_selector.model = RecurrentTask task_selector.task_filter.pop("type") # Fill up the report with the selected tasks tasks = repo.search(task_selector.task_filter, [task_selector.model]) for task in sort_tasks(tasks, sort_criteria): entity_line = [] for attribute in columns: value = getattr(task, attribute) if isinstance(value, list): value = ", ".join(value) elif isinstance(value, datetime): value = _date2str(config, value) elif isinstance(value, Enum): value = value.value.title() elif value is None: value = "" entity_line.append(value) report.add(entity_line) # Clean up the report and print it report._remove_null_columns() report.print()
def test_repository_can_search_by_bool_property( self, repo: Repository, ) -> None: """Search should return the objects that have a bool property.""" expected_entity = BoolEntity(name="Name", active=True) repo.add(expected_entity) repo.commit() result = repo.search({"active": True}, BoolEntity) assert result == [expected_entity]
def test_repository_can_search_by_property_specifying_a_list_of_types( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """Search should return the objects that match the desired property.""" entity_types: List[Type[Entity]] = [type(inserted_entities[0]), OtherEntity] expected_entity = inserted_entities[1] repo.models = entity_types # type: ignore with pytest.warns(UserWarning, match="In 2022-06-10.*deprecated"): result = repo.search({"id_": expected_entity.id_}, entity_types) assert result == [expected_entity]
def test_repo_can_search_in_list_of_str_attribute(self, repo: Repository) -> None: """ Given: A repository with an entity that contains an attribute with a list of str When: search is called with a regexp that matches one of the list elements Then: the entity is returned """ expected_entity = ListEntityFactory.build() repo.add(expected_entity) repo.commit() regexp = rf"{expected_entity.elements[0][:-1]}." result = repo.search({"elements": regexp}, ListEntity) assert result == [expected_entity]
def test_repository_search_returns_empty_list_if_none_found( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """ If no object has the property of the search criteria, return an empty list. """ entity = inserted_entities[0] result = repo.search({"inexistent_field": "inexistent_value"}, type(entity)) assert result == []
def test_search_doesnt_raise_exception_if_search_exception_false( self, repo: Repository ) -> None: """ Given: A repository with search_exception False When: running search on a criteria that returns no results Then: an empty list is returned instead of an exception. See ADR 005 for more info. """ repo = load_repository(search_exception=False) result = repo.search({"id_": "inexistent"}, Author) assert result == []
def test_repository_can_search_by_multiple_properties( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """ Given: a full repository. When: a search is performed by multiple properties. Then: the matching objects are returned. """ entity = inserted_entities[1] result = repo.search({"state": entity.state, "name": entity.name}, type(entity)) assert result == [entity]
def test_repository_can_search_regular_expression( self, repo: Repository, inserted_entities: List[Entity]) -> None: """ Given: More than one entity is inserted in the repository. When: We search using a regular expression Then: The matching entity is found """ expected_entities = [ entity_ for entity_ in inserted_entities if entity_.name == inserted_entities[0].name ] regular_expression = rf"^{expected_entities[0].name}.*" result = repo.search({"name": regular_expression}, type(expected_entities[0])) assert result == expected_entities
def test_repository_can_search_by_property( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """Search should return the objects that match the desired property. And the entity is saved to the cache. The defined_values of all entities are empty, otherwise the merge fails. """ expected_entity = inserted_entities[1] result = repo.search({"id_": expected_entity.id_}, type(inserted_entities[1])) assert result == [expected_entity] assert repo.cache.get(expected_entity) == expected_entity assert result[0].defined_values == {}
def test_repo_can_search_entity_if_two_different_entities_match( self, repo: Repository, ) -> None: """ Given: Two different entities with the same ID When: we search by a property equal in both entities and give only one model. Then: only the entity that matches the model is returned """ author = Author(id_="author_id", name="common name") book = Book(id_=1, name="common name") repo.add(author) repo.add(book) repo.commit() result = repo.search({"name": "common name"}, Author) assert result == [author]
def test_thaw_task_by_id_accepts_state( self, runner: CliRunner, insert_frozen_parent_task_e2e: RecurrentTask, repo_e2e: Repository, caplog: LogCaptureFixture, ) -> None: """Test thawing accepts the thawing state of the tasks.""" parent_task = insert_frozen_parent_task_e2e result = runner.invoke( cli, ["thaw", str(parent_task.id_), "-s", "todo"]) parent_task = repo_e2e.get(parent_task.id_, [RecurrentTask]) child_task = repo_e2e.search({"parent_id": parent_task.id_}, [Task])[0] assert result.exit_code == 0 # T101: fixme found (T O D O). Lol, it's not a comment to fix something assert parent_task.state == TaskState.TODO # noqa: T101 assert child_task.state == TaskState.TODO # noqa: T101
def test_repo_can_search_entity_if_two_different_entities_match_giving_both_models( self, repo: Repository, ) -> None: """ Given: Two different entities with the same ID When: we search by a property equal in both entities and give both models. Then: both entities that matches the model are returned """ author = Author(id_="author_id", name="common name") book = Book(id_=1, name="common name") repo.add(author) repo.add(book) repo.commit() with pytest.warns(UserWarning, match="In 2022-06-10.*deprecated"): result = repo.search({"name": "common name"}, [Author, Book]) assert result == [book, author]
def tags(repo: Repository) -> None: """Print the tags information.""" report = Report(labels=["Name", "Open Tasks"]) open_tasks = repo.search({"active": True}, [Task]) # Gather tags tags: Dict[str, int] = {} for task in open_tasks: if len(task.tags) == 0: tags.setdefault("None", 0) tags["None"] += 1 else: for tag in task.tags: tags.setdefault(tag, 0) tags[tag] += 1 for tag in sorted(tags.keys()): report.add([tag, str(tags[tag])]) report.print()
def areas(repo: Repository) -> None: """Print the areas information.""" report = Report(labels=["Name", "Open Tasks"]) open_tasks = repo.search({"active": True}, [Task]) # Gather areas areas: Dict[str, int] = {} for task in open_tasks: if task.area is None: area = "None" else: area = task.area areas.setdefault(area, 0) areas[area] += 1 for area in sorted(areas.keys()): report.add([area, str(areas[area])]) report.print()
def test_thaw_accepts_filter_of_tasks( self, runner: CliRunner, insert_frozen_parent_tasks_e2e: List[RecurrentTask], repo_e2e: Repository, caplog: LogCaptureFixture, ) -> None: """Test thawing accepts a filter of tasks.""" result = runner.invoke(cli, ["thaw", "state:frozen"]) assert result.exit_code == 0 for parent_task in insert_frozen_parent_tasks_e2e: child_task = repo_e2e.search({"parent_id": parent_task.id_}, [Task])[0] assert ( "pydo.services", logging.INFO, f"Thawed task {parent_task.id_}: {parent_task.description}, and created" f" it's next child task with id {child_task.id_}", ) in caplog.record_tuples
def test_repository_search_by_regular_expression_is_case_insensitive( self, repo: Repository, inserted_entities: List[Entity]) -> None: """ Given: More than one entity is inserted in the repository. When: We search using a regular expression with the wrong capitalization Then: The matching entity is found If you come to disable this functionality, make it configurable instead, being the default the search as case insensitive """ expected_entities = [ entity_ for entity_ in inserted_entities if entity_.name == inserted_entities[0].name ] regular_expression = rf"^{expected_entities[0].name.upper()}.*" result = repo.search({"name": regular_expression}, type(expected_entities[0])) assert result == expected_entities
def thaw_tasks(repo: Repository, selector: TaskSelector, state: Optional[TaskState] = None) -> None: """Thaw a list of tasks based on a task filter.""" selector.model = RecurrentTask selector.task_filter["state"] = TaskState.FROZEN if state is None: state = TaskState.BACKLOG tasks = _tasks_from_selector(repo, selector) if len(tasks) == 0: raise EntityNotFoundError( "No frozen tasks were found with that criteria") for task in tasks: if type(task) == RecurrentTask: task.thaw(state) repo.add(task) try: children = repo.search({ "parent_id": task.id_, "active": False }, [Task]) except EntityNotFoundError: children = [] if len(children) == 0: last_child = None else: last_child = children[-1] child_task = repo.add(task.breed_children(last_child)) log.info( f"Thawed task {task.id_}: {task.description}, and created it's next " f"child task with id {child_task.id_}") repo.commit()
def _tasks_from_selector(repo: Repository, selector: TaskSelector) -> List[TaskType]: """Return the tasks that match the criteria of the task selector.""" tasks: List[TaskType] = [ repo.get(task_id, [selector.model]) for task_id in selector.task_ids ] if selector.task_filter != {}: # Remove the tasks that don't meet the task_filter for task in tasks: # Check if the task_filter is not a subset of the properties of the task. # SIM205: Use 'selector.task_filter.items() > task.dict().items()' instead # No can't do, if we do, the subset checking doesn't work if not selector.task_filter.items() <= task.dict().items( ): # noqa: SIM205 tasks.remove(task) with suppress(EntityNotFoundError): tasks.extend(repo.search(selector.task_filter, [selector.model])) # Remove duplicates return list(set(tasks))