Module jumpscale.clients.taiga.taiga
Taiga client
Initialization
Using username and password:
client = j.clients.taiga.new('test', host="https://staging.circles.threefold.me/", username='admin', password='123456')
OR using a token
client = j.clients.taiga.new('test', host="https://staging.circles.threefold.me/", token='extra secret token string')
Listing
Listing issues
To get the issues of the user with id 123:
client.list_all_issues(123)
To get the issues of all users:
client.list_all_issues()
Listing projects
To list all projects:
client.list_all_projects()
To list all projects not start with ARCHIVE:
client.list_all_active_projects()
Listing user stories
To list the user stories of the user with id 123:
client.list_all_user_stories(123)
To list the user stories of all users:
client.list_all_user_stories()
Listing tasks
To list the tasks of the user with id 123:
client.list_all_tasks(123)
To list the tasks of all users :
client.list_all_tasks()
List team circles
client.list_team_circles()
List project circles
client.list_project_circles()
List funnel circles
client.list_funnel_circles()
Custom Fields
Get
To get issue custom fields for the issue with id 123
custom_fields = client.get_issue_custom_fields(123)
To get user story custom fields for the story with id 123
custom_fields = client.get_story_custom_fields(123)
Validate
To validate custom field according to specs
client.validate_custom_fields(custom_fields)
Creating
Create new circle
if you want full control on the circle creation on priorities, severities, .. etc, you can use _create_new_circle method
def _create_new_circle(
    self,
    name,
    type_="team",
    description="desc",
    severities=None,
    issues_statuses=None,
    priorities=None,
    issues_types=None,
    user_stories_statuses=None,
    tasks_statuses=None,
    custom_fields=None
    **attrs,
):
otherwise you can use create_new_project_circle,, create_new_team_circle, create_new_funnel_circle
Create new story
circle_object.create_story("abc")
Create a new issue
create_issue("my issue")
Exporting
Export users and circles
client.export_as_md("/tmp/taigawiki")
Export users
client.export_users_as_md("/tmp/taigawiki")
Export circles
client.export_circles_as_md("/tmp/taigawiki")
Export users and circles periodically
To export users and circles periodically each 10 minutes
client.export_as_md_periodically("/tmp/taigawiki", period= 600)
period use seconds as time unit.
Export objects as yaml
To export All objects as yaml all you need is
client.export_as_yaml("/tmp/exported_taiga_dir")
This will export resources (users, projects, issues, stories, tasks) in `/tmp/exported_taiga_dir/$object_type/$object_id.yaml
Importing
Importing from yaml files
To import from yaml files files which exported using export_as_yaml
client.import_from_yaml("/tmp/exported_taiga_dir")
This will import resources (projects, issues, stories, tasks) as a new instance import basic info till now
Operations
Move a story to a project
client.move_story_to_circle(789, 123) # story id, project id
Copy and Move Issue using project object
project_object.copy_issue(issue_id_or_issue_object, project_id_or_project_object)
project_object.move_issue(issue_id_or_issue_object, project_id_or_project_object)
Keep in mind that move will delete the issue from the original project
Resources urls
All of resources e.g (user, issue, user_story, circle, task) have url, as_md and as_yaml properties
Expand source code
"""
# Taiga client
## Initialization
Using username and  password:
```
client = j.clients.taiga.new('test', host="https://staging.circles.threefold.me/", username='admin', password='123456')
```
OR using a token
```
client = j.clients.taiga.new('test', host="https://staging.circles.threefold.me/", token='extra secret token string')
```
## Listing
### Listing issues
To get the issues of the user with id 123:
```
client.list_all_issues(123)
```
To get the issues of all users:
```
client.list_all_issues()
```
### Listing projects
To list all projects:
```
client.list_all_projects()
```
To list all projects not start with **ARCHIVE**:
```
client.list_all_active_projects()
```
### Listing user stories
To list the user stories of the user with id 123:
```
client.list_all_user_stories(123)
```
To list the user stories of all users:
```
client.list_all_user_stories()
```
### Listing tasks
To list the tasks of the user with id 123:
```
client.list_all_tasks(123)
```
To list the tasks of all users :
```
client.list_all_tasks()
```
### List team circles
```
client.list_team_circles()
```
### List project circles
```
client.list_project_circles()
```
### List funnel circles
```
client.list_funnel_circles()
```
## Custom Fields
### Get
To get issue custom fields for the issue with id 123
```
custom_fields = client.get_issue_custom_fields(123)
```
To get user story custom fields for the story with id 123
```
custom_fields = client.get_story_custom_fields(123)
```
### Validate
To validate custom field according to [specs](https://github.com/threefoldtech/circles_reporting_tool/blob/master/specs/funnel.md#custom-fields)
```
client.validate_custom_fields(custom_fields)
```
## Creating
### Create new circle
if you want full control on the circle creation on priorities, severities, .. etc, you can use `_create_new_circle` method
```
def _create_new_circle(
    self,
    name,
    type_="team",
    description="desc",
    severities=None,
    issues_statuses=None,
    priorities=None,
    issues_types=None,
    user_stories_statuses=None,
    tasks_statuses=None,
    custom_fields=None
    **attrs,
):
```
otherwise you can use `create_new_project_circle,`, `create_new_team_circle`, `create_new_funnel_circle`
### Create new story
```
circle_object.create_story("abc")
```
### Create a new  issue
```
create_issue("my issue")
```
## Exporting
### Export users and circles
```
client.export_as_md("/tmp/taigawiki")
```
### Export users
```
client.export_users_as_md("/tmp/taigawiki")
```
### Export circles
```
client.export_circles_as_md("/tmp/taigawiki")
```
### Export users and circles periodically
To export users and circles periodically each 10 minutes
```
client.export_as_md_periodically("/tmp/taigawiki", period= 600)
```
> **period** use seconds as time unit.
### Export objects as yaml
To export All objects as yaml all you need is
```
client.export_as_yaml("/tmp/exported_taiga_dir")
```
This will export resources (users, projects, issues, stories, tasks) in `/tmp/exported_taiga_dir/$object_type/$object_id.yaml
## Importing
### Importing from yaml files
To import from yaml files _files which exported using export_as_yaml_
```
client.import_from_yaml("/tmp/exported_taiga_dir")
```
This will import resources (projects, issues, stories, tasks) as a new instance _import basic info till now_
## Operations
### Move a story to a project
```
client.move_story_to_circle(789, 123) # story id, project id
```
### Copy and Move Issue using project object
```
project_object.copy_issue(issue_id_or_issue_object, project_id_or_project_object)
project_object.move_issue(issue_id_or_issue_object, project_id_or_project_object)
```
> Keep in mind that move will delete the issue from the original project
### Resources urls
All of resources e.g (user, issue, user_story, circle, task) have `url, as_md and as_yaml` properties
"""
import copy
from collections import defaultdict
from functools import lru_cache
from textwrap import dedent
import dateutil
import dateutil.utils
import gevent
from gevent.event import Event
import yaml
from jumpscale.clients.base import Client
from jumpscale.clients.taiga.models import (
    Circle,
    CircleIssue,
    CircleStory,
    CircleTask,
    CircleUser,
    FunnelCircle,
    ProjectCircle,
    TeamCircle,
)
from jumpscale.core.base import fields
from jumpscale.loader import j
from taiga import TaigaAPI
from taiga.exceptions import TaigaRestException
from taiga.models.models import Milestones
from pathlib import Path
class TaigaClient(Client):
    def credential_updated(self, value):
        self._api = None
    host = fields.String(default="https://projects.threefold.me")
    username = fields.String(on_update=credential_updated)
    password = fields.Secret(on_update=credential_updated)
    token = fields.Secret(on_update=credential_updated)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._api = None
        self.text = ""
    def __hash__(self):
        return hash(str(self))
    @property
    def api(self):
        if not self._api:
            api = TaigaAPI(host=self.host)
            if self.token:
                api.token = self.token
            else:
                if not self.username or not self.password:
                    raise j.exceptions.Runtime("Token or username and password are required")
                api.auth(self.username, self.password)
            self._api = api
        return self._api
    @lru_cache(maxsize=2048)
    def _get_project(self, project_id):
        return self.api.projects.get(project_id)
    @lru_cache(maxsize=2048)
    def _get_milestone(self, milestone_id):
        if milestone_id:
            return self.api.milestones.get(milestone_id)
    @lru_cache(maxsize=2048)
    def _get_priority(self, priority_id):
        return self.api.priorities.get(priority_id)
    @lru_cache(maxsize=2048)
    def _get_assignee(self, assignee_id):
        return CircleUser(self, self.api.users.get(assignee_id))
    _get_user_by_id = _get_assignee
    def _get_users_by_ids(self, ids=None):
        ids = ids or []
        return [self._get_user_by_id(x) for x in ids]
    def _get_issues_by_ids(self, ids=None):
        ids = ids or []
        return [self._get_issue_by_id(x) for x in ids]
    def _get_task_by_id(self, id):
        return self.api.tasks.get(id)
    @lru_cache(maxsize=2048)
    def _get_issue_status(self, status_id):
        return self.api.issue_statuses.get(status_id)
    @lru_cache(maxsize=2048)
    def _get_user_stories_status(self, status_id):
        return self.api.user_story_statuses.get(status_id)
    @lru_cache(maxsize=2048)
    def _get_task_status(self, status_id):
        return self.api.task_statuses.get(status_id)
    @lru_cache(maxsize=2048)
    def _get_user_id(self, username):
        user = self.api.users.list(username=username)
        if user:
            user = user[0]
            return user.id
        else:
            raise j.exceptions.Input(f"Couldn't find user with username: {username}")
    @lru_cache(maxsize=2048)
    def _get_user_by_name(self, username):
        theid = self._get_user_id(username)
        return self._get_user_by_id(theid)
    def get_issue_custom_fields(self, id):
        """Get Issue Custom fields
        Args:
            id (int): Issue id
        Returns:
            List: List of dictionaries {name: "custom field name", value: {values as dict}}
        """
        issue = self.api.issues.get(id)
        issue_attributes = issue.get_attributes()["attributes_values"]
        project_attributes = self._get_project(issue.project).list_issue_attributes()
        custom_fields = []
        for p_attr in project_attributes:
            for k, value in issue_attributes.items():
                if p_attr.id == int(k):
                    try:
                        custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)})
                    except:
                        custom_fields.append({"name": p_attr.name, "value": value})
                    break
        return custom_fields
    def get_story_custom_fields(self, id):
        """Get User_Story Custom fields
        Args:
            id (int): User_Story id
        Returns:
            List: List of dictionaries {name: "custom field name", value: {values as dict}}
        """
        user_story = self.api.user_stories.get(id)
        user_story_attributes = user_story.get_attributes()["attributes_values"]
        project_attributes = self._get_project(user_story.project).list_user_story_attributes()
        custom_fields = []
        for p_attr in project_attributes:
            for k, value in user_story_attributes.items():
                if p_attr.id == int(k):
                    try:
                        custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)})
                    except:
                        custom_fields.append({"name": p_attr.name, "value": value})
                    break
        return custom_fields
    def get_user_circles(self, username):
        """Get circles owned by user
        Args:
            username (str): Name of the user
        """
        user_id = self._get_user_id(username)
        circles = self.api.projects.list(member=user_id)
        user_circles = []
        for circle in circles:
            if circle.owner["id"] == user_id:
                user_circles.append(self._resolve_object(circle))
        return user_circles
    def get_circles_issues(self, project_id):
        """Get all issues in a circle/project
        Args:
            project_id (int): id of the circle/project
        Raises:
            j.exceptions.NotFound: if couldn't find circle with specified id
        """
        try:
            circle = self.api.projects.get(project_id)
        except TaigaRestException:
            raise j.exceptions.NotFound(f"Couldn't find project with id: {project_id}")
        circle_issues = []
        for issue in circle.list_issues():
            issue.project = self._get_project(issue.project)
            issue.milestone = self._get_milestone(issue.milestone)
            issue.priority = self._get_priority(issue.priority)
            issue.assignee = self._get_assignee(issue.assigned_to)
            issue.status = self._get_issue_status(issue.status)
            circle_issues.append(issue)
        return circle_issues
    def get_user_stories(self, username):
        """Get all stories of a user
        Args:
            username (str): Name of the user
        """
        user_id = self._get_user_id(username)
        user_stories = self.api.user_stories.list(assigned_to=user_id)
        user_stories = []
        for user_story in user_stories:
            # user_story.project = self._get_project(user_story.project)
            # user_story.milestone = self._get_milestone(user_story.milestone)
            user_story.status = self._get_user_stories_status(user_story.status)
            user_stories.append(user_story)
        return user_stories
    def get_user_tasks(self, username):
        """Get all tasks of a user
        Args:
            username (str): Name of the user
        """
        user_id = self._get_user_id(username)
        user_tasks = self.api.tasks.list(assigned_to=user_id)
        user_tasks = []
        for user_task in user_tasks:
            # user_task.project = self._get_project(user_task.project)
            # user_task.milestone = self._get_milestone(user_task.milestone)
            user_task.status = self._get_task_status(user_task.status)
            user_tasks.append(self._resolve_object(user_task))
        return user_tasks
    def move_story_to_circle(self, story_id, project_id):
        """Moves a story to another circle/project
        Args:
            story_id (int): User story id
            project_id (int): circle/project id
        Raises:
            j.exceptions.NotFound: No user story with specified id found
            j.exceptions.NotFound: No project with specified id found
            j.exceptions.Runtime: [description]
        Returns:
            int: New id of the migrated user story
        """
        def _get_project_status(project_statuses, status):
            for project_status in project_statuses:
                if project_status.name == status:
                    return project_status.id
        try:
            user_story = self.api.user_stories.get(story_id)
        except TaigaRestException:
            raise j.exceptions.NotFound(f"Couldn't find user story with id: {story_id}")
        project_stories_statuses = self.api.user_story_statuses.list(project=project_id)
        status = self._get_user_stories_status(user_story.status)
        story_status_id = _get_project_status(project_stories_statuses, status)
        try:
            migrate_story = self.api.user_stories.create(
                project=project_id,
                subject=user_story.subject,
                assigned_to=user_story.assigned_to,
                milestone=user_story.milestone,
                status=story_status_id,
                tags=user_story.tags,
            )
        except TaigaRestException:
            raise j.exceptions.NotFound(f"No project with id: {project_id} found")
        try:
            comments = self.api.history.user_story.get(story_id)
            comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"]))
            for comment in comments:
                migrate_story.add_comment(comment["comment_html"])
            project_tasks_statuses = self.api.task_statuses.list(project=project_id)
            for task in user_story.list_tasks():
                status = self._get_task_status(task.status)
                task_status_id = _get_project_status(project_tasks_statuses, status)
                migrate_task = migrate_story.add_task(
                    subject=task.subject,
                    status=task_status_id,
                    due_date=task.due_date,
                    milestone=task.milestone,
                    assigned_to=task.assigned_to,
                    tags=task.tags,
                    project=migrate_story.project,
                    user_story=migrate_story.id,
                )
                comments = self.api.history.task.get(migrate_task.id)
                comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"]))
                for comment in comments:
                    migrate_task.add_comment(comment["comment_html"])
        except Exception as e:
            self.api.user_stories.delete(migrate_story.id)
            raise j.exceptions.Runtime(f"Failed to migrate story error was: {str(e)}")
        self.api.user_stories.delete(story_id)
        return migrate_story.id
    def list_all_issues(self, username="", full_info=False):
        """
        List all issues for specific user if you didn't pass user_id will list all the issues
        HINT: Using full_info will take a longer time
        Args:
            username (str): username.
            full_info (bool): flag used to get object with full info. Defaults to False.
        Returns:
            List: List of taiga.models.models.Issue.
        """
        if username:
            user_id = self._get_user_id(username)
            if not full_info:
                return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list(assigned_to=user_id)]
            else:
                return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list(assigned_to=user_id)]
        else:
            if not full_info:
                return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list()]
            else:
                return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list()]
    def list_all_tasks(self, username="", full_info=False):
        """
        List all tasks for specific user if you didn't pass user_id will list all the tasks
        HINT: Using full_info will take a longer time
        Args:
            username (str): username.
            full_info (bool): flag used to get object with full info. Defaults to False.
        Returns:
            List: List of taiga.models.models.Task.
        """
        if username:
            user_id = self._get_user_id(username)
            if not full_info:
                return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list(assigned_to=user_id)]
            else:
                return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list(assigned_to=user_id)]
        else:
            if not full_info:
                return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list()]
            else:
                return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list()]
    def list_all_projects(self, full_info=False):
        """
        List all projects
        HINT: Using full_info will take a longer time
        Args:
            full_info(bool): flag used to get object with full info. Defaults to False.
        Returns:
            List: List of taiga.models.models.Project.
        """
        if not full_info:
            return [Circle(self, self._resolve_object(x)) for x in self.api.projects.list()]
        else:
            return [Circle(self, self.api.projects.get(x.id)) for x in self.api.projects.list()]
    def list_all_active_projects(self, full_info=False):
        """
        List all projects not starting with "ARHCIVE"
        HINT: Using full_info will take a longer time
        Args:
            full_info (bool): [description]. Defaults to False.
        Returns:
            [type]: [description]
        """
        return [
            Circle(self, p)
            for p in self.list_projects_by(lambda x: not x.name.startswith("ARCHIVE_"), full_info=full_info)
        ]
    def list_all_milestones(self):
        """
        List all milestones
        Returns:
            List: List of taiga.models.models.Milestone.
        """
        return [self._resolve_object(x) for x in self.api.milestones.list()]
    def list_all_user_stories(self, username="", full_info=False):
        """
        List all user stories for specific user if you didn't pass user_id will list all the available user stories
        HINT: Using full_info will take a longer time
        Args:
            username (str): username.
            full_info(bool): flag used to get object with full info. Defaults to False
        Returns:
            List: List of CircleStory.
        """
        if username:
            user_id = self._get_user_id(username)
            if not full_info:
                return [
                    CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list(assigned_to=user_id)
                ]
            else:
                return [
                    CircleStory(self, self.api.user_stories.get(x.id))
                    for x in self.api.user_stories.list(assigned_to=user_id)
                ]
        else:
            if not full_info:
                return [CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list()]
            else:
                return [CircleStory(self, self.api.user_stories.get(x.id)) for x in self.api.user_stories.list()]
    def list_all_users(self, full_info=False):
        """
        List all user stories for specific user if you didn't pass user_id will list all the available user stories
        HINT: Using full_info will take a longer time
        Args:
            username (str): username.
            full_info(bool): flag used to get object with full info. Defaults to False
        Returns:
            List: List of CircleUser.
        """
        circles = self.list_all_projects()
        users = set()
        for c in circles:
            for m in c.members:
                users.add(m)
        return [CircleUser(self, self._get_user_by_id(uid)) for uid in users]
    def get_issue_by_id(self, issue_id):
        """Get issue
        Args:
            issue_id: the id of the desired issue
        Returns:
            Issue object: issue
        """
        return CircleIssue(self, self.api.issues.get(issue_id))
    def _resolve_object(self, obj):
        resolvers = {
            "owners": self._get_users_by_ids,
            "watchers": self._get_users_by_ids,
            "members": self._get_users_by_ids,
            "project": self._get_project,
            "circle": self._get_project,
            "milestone": self._get_milestone,
            "task_status": self._get_task_status,
            "assigned_to": self._get_user_by_id,
            "owner": self._get_user_by_id,
            "issues": self._get_issues_by_ids,
            "tasks": self._get_task_by_id,
        }
        newobj = copy.deepcopy(obj)
        for k in dir(newobj):
            v = getattr(newobj, k)
            if isinstance(v, int) or isinstance(v, list) and v and isinstance(v[0], int):
                if k in resolvers:
                    resolved = None
                    resolver = resolvers[k]
                    try:
                        copied_v = copy.deepcopy(v)
                        resolved = lambda: resolver(copied_v)
                        if isinstance(v, list):
                            setattr(newobj, f"{k}_objects", resolved)
                        else:
                            setattr(newobj, f"{k}_object", resolved)
                    except Exception as e:
                        import traceback
                        traceback.print_exc()
                        j.logger.error(f"error {e}")
        return newobj
    def list_projects_by(self, fn=lambda x: True, full_info=False):
        return [p for p in self.list_all_projects(full_info=full_info) if fn(p)]
    def list_team_circles(self):
        return [TeamCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("TEAM_"))]
    def list_project_circles(self):
        return [ProjectCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("PROJECT_"))]
    def list_funnel_circles(self):
        return [FunnelCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("FUNNEL_"))]
    def validate_custom_fields(self, attributes):
        """Validate custom fields values to match our requirements
        Args:
            attributes (List): Output from get_issue/story_custom_fields functions
        Raises:
            j.exceptions.Validation: Raise validation exception if any input not valid
        Returns:
            bool: Return True if no exception raised and print logs
        """
        for attr in attributes:
            name = attr.get("name")
            value = attr.get("value")
            period = value.get("period", "onetime")
            duration = value.get("duration", 1)
            amount = value.get("amount", 0)
            currency = value.get("currency", "eur")
            start_date = value.get("start_date", f"{dateutil.utils.today().month}:{dateutil.utils.today().year}",)
            confidence = value.get("confidence", 100)
            user = value.get("user")
            part = value.get("part", "0%")
            type = value.get("type", "revenue")
            if name not in ["bookings", "commission"]:
                raise j.exceptions.Validation(
                    f'Name: ({name}) is unknown custom field, please select one of the following ["bookings", "commission"]'
                )
            if period not in ["onetime", "month", "year"]:
                raise j.exceptions.Validation(
                    f'Period: ({period}) not found, please select one of following ["onetime", "month", "year"]'
                )
            if duration < 1 or duration > 120:
                raise j.exceptions.Validation(f"Duration: ({duration}) is not in range, please select it from 1 to 120")
            if not isinstance(amount, int):
                raise j.exceptions.Validation(f"Amount: ({amount}) is not integer, please add int value")
            if currency.replace(" ", "").lower() not in [
                "usd",
                "chf",
                "eur",
                "gbp",
                "egp",
            ]:
                raise j.exceptions.Validation(
                    f'Currency: ({currency}) is not supported, please use one of the following currencies ["usd", "chf", "eur", "gbp", "egp"]'
                )
            try:
                date = start_date.split(":")
                month = int(date[0])
                year = int(date[1]) if len(date) > 1 else dateutil.utils.today().year
                if month < 1 or month > 12:
                    raise j.exceptions.Validation(
                        "Please use values from 1 to 12 in Month field, follow format like MONTH:YEAR as 11:2020 or MONTH as 11"
                    )
            except ValueError as e:
                raise j.exceptions.Validation(
                    "Please use numeric date with the following format MONTH:YEAR as 11:2020 or MONTH as 11"
                )
            except AttributeError as e:
                pass  # Will check what happen if start_date not provide
            if confidence % 10 != 0:
                j.exceptions.Validation(f"Confidence: ({confidence}) not multiple of 10, it must be multiple of 10")
            part_tmp = part.replace("%", "")
            if user != None and user not in self.list_all_users():
                raise j.exceptions.Validation(f"User: ({user}) is not found")
            if int(part_tmp) < 0 or int(part_tmp) > 100:
                j.exceptions.Validation(f"Part: ({part}) is a not a valid percentage, it must be from 0% to 100%")
            if type not in ["revenue", "booking"]:
                raise j.exceptions.Validation(
                    f'Type: ({type}) is not supported type, please choose one of the following ["revenue" , "booking"]'
                )
            j.logger.info(f"Attribute: {name} passed")
        return True
    def _create_new_circle(
        self,
        name,
        type_="team",
        description="desc",
        severities=None,
        issues_statuses=None,
        priorities=None,
        issues_types=None,
        user_stories_statuses=None,
        tasks_statuses=None,
        custom_fields=None,
        **attrs,
    ):
        severities = severities or ["Low", "Mid", "High"]
        priorities = priorities or [
            "Wishlist",
            "Minor",
            "Normal",
            "Important",
            "Critical",
        ]
        issues_statuses = issues_statuses or [
            "New",
            "In progress",
            "Ready for test",
            "Closed",
            "Needs Info",
            "Rejected",
            "Postponed",
        ]
        issues_types = issues_types or []
        user_stories_statuses = user_stories_statuses or []
        tasks_statuses = tasks_statuses or []
        custom_fields = custom_fields or []
        type_ = type_.upper()
        project_name = f"{type_}_{name}"
        p = self.api.projects.create(project_name, description=description)
        for t in tasks_statuses:
            try:
                p.add_task_status(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping task {t} {e}")
        for t in priorities:
            try:
                p.add_priority(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping prio {t} {e}")
        for t in severities:
            try:
                p.add_severity(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping sever {t} {e}")
        for t in issues_statuses:
            try:
                p.add_issue_status(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping status {t} {e}")
        for t in user_stories_statuses:
            try:
                p.add_user_story_status(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping user status {t} {e}")
        for t in issues_types:
            try:
                p.add_issue_type(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping issue type {t} {e}")
        for t in custom_fields:
            try:
                p.add_issue_attribute(t)
                p.add_user_story_attribute(t)
            except Exception as e:
                # check if duplicated
                j.logger.debug(f"skipping custom field type {t} {e}")
        return p
    def create_new_project_circle(
        self, name, description="", **attrs,
    ):
        """Creates a new project circle.
        Args:
            name (str): circle name
            description (str, optional): circle description. Defaults to "".
        Returns:
            [ProjectCircle]: Project circle
        """
        attrs = {
            "is_backlog_activated": False,
            "is_issues_activated": True,
            "is_kanban_activated": True,
            "is_private": False,
            "is_wiki_activated": True,
        }
        issues_types = ["Bug", "Question", "Enhancement"]
        severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"]
        priorities = None
        story_statuses = [
            "New",
            "to-start",
            "in-progress",
            "Blocked",
            "Implemented",
            "Verified",
            "Archived",
        ]
        item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"]
        issues_statuses = [
            "New",
            "to-start",
            "in-progress",
            "Blocked",
            "Implemented",
            "Closed",
            "Rejected",
            "Postponed",
            "Archived",
        ]
        return ProjectCircle(
            self,
            self._create_new_circle(
                name,
                type_="project",
                description=description,
                severities=severities,
                issues_statuses=issues_statuses,
                priorities=priorities,
                issues_types=issues_types,
                user_stories_statuses=story_statuses,
                tasks_statuses=item_statuses,
                **attrs,
            ),
        )
    def create_new_team_circle(self, name, description="", **attrs):
        """Creates a new team circle. using sprints & timeline (does not use kanban)
        Args:
            name (str): circle name
            description (str, optional): circle description. Defaults to "".
            severities (List[str], optional): list of strings to represent severities. Defaults to None.
            issues_statuses (List[str], optional): list of strings to represent issues_stauses. Defaults to None.
            priorities (List[str], optional): list of strings to represent priorities. Defaults to None.
            issues_types (List[str], optional): list of strings to represent issues types. Defaults to None.
            user_stories_statuses (List[str], optional): list of strings to represent user stories. Defaults to None.
            tasks_statuses (List[str], optional): list of strings to represent task statuses. Defaults to None.
        Returns:
            [TeamCircle]: team circle
        """
        attrs = {
            "is_backlog_activated": True,
            "is_issues_activated": True,
            "is_kanban_activated": False,
            "is_private": False,
            "is_wiki_activated": True,
        }
        issues_types = ["Bug", "Question", "Enhancement"]
        severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"]
        priorities = None
        story_statuses = [
            "New",
            "to-start",
            "in-progress",
            "Blocked",
            "Implemented",
            "Verified",
            "Archived",
        ]
        item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"]
        issues_statuses = [
            "New",
            "to-start",
            "in-progress",
            "Blocked",
            "Implemented",
            "Closed",
            "Rejected",
            "Postponed",
            "Archived",
        ]
        return TeamCircle(
            self,
            self._create_new_circle(
                name,
                type_="team",
                description=description,
                severities=severities,
                issues_statuses=issues_statuses,
                priorities=priorities,
                issues_types=issues_types,
                user_stories_statuses=story_statuses,
                tasks_statuses=item_statuses,
                **attrs,
            ),
        )
    def create_new_funnel_circle(self, name, description="", **attrs):
        """Creates a new funnel circle. using sprints & timeline (does not use kanban)
        Args:
            name (str): circle name
            description (str, optional): circle description. Defaults to "".
        Returns:
            [FunnelCircle]: funnel circle
        """
        attrs = {
            "is_backlog_activated": False,
            "is_issues_activated": True,
            "is_kanban_activated": True,
            "is_private": False,
            "is_wiki_activated": True,
        }
        severities = ["unknown", "low", "25%", "50%", "75%", "90%"]
        priorities = ["Low", "Normal", "High"]
        issues_types = "opportunity"
        issues_statuses = [
            "New",
            "Interested",
            "Deal",
            "Blocked",
            "NeedInfo",
            "Lost",
            "Postponed",
            "Won",
        ]
        story_statuses = [
            "New",
            "Proposal",
            "Contract",
            "Blocked",
            "NeedInfo",
            "Closed",
        ]
        task_statuses = ["New", "In progress", "Verification", "Needs info", "Closed"]
        custom_fields = ["bookings", "commission"]
        return FunnelCircle(
            self,
            self._create_new_circle(
                name,
                type_="funnel",
                description=description,
                severities=severities,
                issues_statuses=issues_statuses,
                priorities=priorities,
                issues_types=issues_types,
                user_stories_statuses=story_statuses,
                tasks_statuses=task_statuses,
                custom_fields=custom_fields,
                **attrs,
            ),
        )
    def export_circles_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False):
        """export circles into {wikipath}/src/circles
        HINT: Using full_info will take longer time
        Args:
            wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki".
            full_info (bool): export object with full info. Defaults to False
        """
        path = j.sals.fs.join_paths(wikipath, "src", "circles")
        j.sals.fs.mkdirs(path)
        circles = self.list_all_active_projects(full_info=full_info)
        def write_md_for_circle(circle):
            circle_md = circle.as_md
            circle_mdpath = j.sals.fs.join_paths(path, f"{circle.clean_name}.md")
            if not (
                modified_only and j.sals.fs.exists(circle_mdpath) and j.sals.fs.read_ascii(circle_mdpath) == circle_md
            ):
                j.sals.fs.write_ascii(circle_mdpath, circle_md)
        circles_mdpath = j.sals.fs.join_paths(path, "circles.md")
        circles_mdcontent = "# circles\n\n"
        for c in circles:
            circles_mdcontent += f"- [{c.name}](./{c.clean_name}.md)\n"
        j.sals.fs.write_ascii(circles_mdpath, circles_mdcontent)
        greenlets = [gevent.spawn(write_md_for_circle, gcircle_obj) for gcircle_obj in circles]
        gevent.joinall(greenlets)
    def export_users_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False):
        """export users into {wikipath}/src/users
        HINT: Using full_info will take longer time
        Args:
            wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki".
            modified_only (bool): export moidified objects only
            full_info (bool): export object with full info. Defaults to False
        """
        path = j.sals.fs.join_paths(wikipath, "src", "users")
        j.sals.fs.mkdirs(path)
        users_objects = self.list_all_users(full_info=full_info)
        users_mdpath = j.sals.fs.join_paths(path, "users.md")
        users_mdcontent = "# users\n\n"
        def write_md_for_user(user):
            user_md = user.as_md
            user_mdpath = j.sals.fs.join_paths(path, f"{user.clean_name}.md")
            if not (modified_only and j.sals.fs.exists(user_mdpath) and j.sals.fs.read_ascii(user_mdpath) == user_md):
                j.sals.fs.write_ascii(user_mdpath, user_md)
        for u in users_objects:
            users_mdcontent += f"- [{u.username}](./{u.clean_name}.md)\n"
        j.sals.fs.write_ascii(users_mdpath, users_mdcontent)
        greenlets = [gevent.spawn(write_md_for_user, guser_obj) for guser_obj in users_objects]
        gevent.joinall(greenlets)
    def export_as_md(self, wiki_path="/tmp/taigawiki", modified_only: bool = True, full_info=False):
        """export taiga instance into a wiki  showing users and circles
        HINT: Using full_info will take longer time
        Args:
            wiki_src_path (str, optional): wiki path. Defaults to "/tmp/taigawiki".
            modified_only (bool): write modified objects only. Defaults to True
            full_info (bool): export object with full info. Defaults to False
        """
        j.logger.info("Start Exporting Wiki ...")
        gs = []
        gs.append(gevent.spawn(self.export_circles_as_md, wiki_path, modified_only, full_info))
        gs.append(gevent.spawn(self.export_users_as_md, wiki_path, modified_only, full_info))
        gevent.joinall(gs)
        template_file = j.sals.fs.join_paths(Path(__file__).parent, "template.html")
        index_html_path = j.sals.fs.join_paths(wiki_path, "src", "index.html")
        readme_md_path = j.sals.fs.join_paths(wiki_path, "src", "README.md")
        sidebar_md_path = j.sals.fs.join_paths(wiki_path, "src", "_sidebar.md")
        content = dedent(
            f"""
            # Taiga overview
            - [circles](./circles/circles.md)
            - [users](./users/users.md)
        """
        )
        j.sals.fs.write_ascii(readme_md_path, content)
        j.sals.fs.write_ascii(sidebar_md_path, content)
        j.sals.fs.copy_file(template_file, index_html_path)
        j.logger.info(f"Exported at {wiki_path}")
    def export_as_md_periodically(
        self, wiki_path="/tmp/taigawiki", period: int = 300, modified_only: bool = True, full_info=False
    ):
        """export taiga instance into a wiki  showing users and circles periodically
        HINT: Using full_info will take longer time
        Args:
            wiki_path (str, optional): wiki path. Defaults to "/tmp/taigawiki".
            period (int): Time to wait between each export in "Seconds". Defaults to 300 (5 Min).
            modified_only (bool): write modified objects only.. Defaults to True.
            full_info (bool): export object with full info. Defaults to False
        """
        repeater = Event()
        while True:
            j.logger.info("Start Exporting ....")
            self.export_as_md(wiki_path, modified_only, full_info)
            j.logger.info(f"Exported at {wiki_path}")
            repeater.wait(period)
    def export_as_yaml(self, export_dir="/tmp/export_dir", full_info=False):
        """export taiga instance [Circle, Story, Issue, Task , User] into a yaml files
        HINT: Using full_info will take longer time
        Args:
            export_dir (str, optional): [description]. Defaults to "/tmp/export_dir".
            full_info (bool): export object with full info. Defaults to False
        """
        def _export_objects_to_dir(objects_dir, objects_fun, full_info):
            j.sals.fs.mkdirs(objects_dir)
            objects = objects_fun(full_info=full_info)
            for obj in objects:
                try:
                    outpath = j.sals.fs.join_paths(objects_dir, f"{obj.id}.yaml")
                    j.sals.fs.write_ascii(outpath, obj.as_yaml)
                except Exception as e:
                    import traceback
                    traceback.print_exc()
                    j.logger.error(e)
                    j.logger.error(f"{type(obj)}: {obj.id}")
        projects_path = j.sals.fs.join_paths(export_dir, "projects")
        stories_path = j.sals.fs.join_paths(export_dir, "stories")
        issues_path = j.sals.fs.join_paths(export_dir, "issues")
        tasks_path = j.sals.fs.join_paths(export_dir, "tasks")
        # Milestones is not one of our model objects
        # milestones_path = j.sals.fs.join_paths(export_dir, "milestones")
        users_path = j.sals.fs.join_paths(export_dir, "users")
        def on_err(*args, **kwargs):
            print("err, ", args, kwargs)
        j.logger.info("Start Export as YAML")
        gs = []
        gs.append(gevent.spawn(_export_objects_to_dir, projects_path, self.list_all_active_projects, full_info))
        gs.append(gevent.spawn(_export_objects_to_dir, stories_path, self.list_all_user_stories, full_info))
        gs.append(gevent.spawn(_export_objects_to_dir, issues_path, self.list_all_issues, full_info))
        # Milestones is not one of our model objects
        # gs.append(gevent.spawn(_export_objects_to_dir, milestones_path, self.list_all_milestones)
        gs.append(gevent.spawn(_export_objects_to_dir, users_path, self.list_all_users, full_info))
        gs.append(gevent.spawn(_export_objects_to_dir, tasks_path, self.list_all_tasks, full_info))
        gevent.joinall(gs)
        j.logger.info("Finish Export as YAML")
    def import_from_yaml(self, import_dir="/tmp/export_dir"):
        """Import Circle with all stories, issues and tasks from yaml files
        Args:
            import_dir (str): import directory path. Defaults to "/tmp/export_dir".
        """
        # Helper Functions
        def check_by_name(list_obj, name_to_find):
            for obj in list_obj:
                if obj.name == name_to_find:
                    return obj.id
            return list_obj[0].id
        def import_circle(self, yaml_obj):
            circle = None
            # Funnel Circle
            if yaml_obj["basic_info"]["name"].lower() == "funnel":
                circle = self.create_new_funnel_circle(
                    yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],
                )
            # Team Circle
            elif yaml_obj["basic_info"]["name"].lower() == "team":
                circle = self.create_new_team_circle(
                    yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],
                )
            # Project Circle
            elif yaml_obj["basic_info"]["name"].lower() == "project":
                circle = self.create_new_project_circle(
                    yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],
                )
            # Any Other Circle
            else:
                circle = self._create_new_circle(yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],)
                circle.is_backlog_activated = yaml_obj["modules"]["is_backlog_activated"]
                circle.is_issues_activated = yaml_obj["modules"]["is_issues_activated"]
                circle.is_kanban_activated = yaml_obj["modules"]["is_kanban_activated"]
                circle.is_wiki_activated = yaml_obj["modules"]["is_wiki_activated"]
            circle.is_private = yaml_obj["basic_info"]["is_private"]
            circle.videoconferences = yaml_obj["modules"]["videoconferences"]
            for issue_attr in yaml_obj["issues_attributes"]:
                circle.add_issue_attribute(issue_attr)
            for us_attr in yaml_obj["stories_attributes"]:
                circle.add_user_story_attribute(us_attr)
            return circle
        def import_story(circle_object, yaml_obj):
            status_id = check_by_name(circle_object.us_statuses, yaml_obj["status"]["name"])
            story = circle_object.add_user_story(
                yaml_obj["basic_info"].get("subject"),
                tags=yaml_obj["basic_info"].get("tags"),
                description=yaml_obj["basic_info"].get("description", ""),
                client_requirement=yaml_obj["requirements"].get("client_requirement"),
                team_requirement=yaml_obj["requirements"].get("team_requirement"),
                is_blocked=yaml_obj["additional_info"].get("is_blocked"),
                due_date=yaml_obj["date"].get("due_date"),
                status=status_id,
            )
            for field in yaml_obj.get("custom_fields", []):
                for attr in circle_object.list_user_story_attributes():
                    if attr.name == field["name"]:
                        story.set_attribute(attr.id, field["value"])
                        break
            return story
        def import_issue(circle_object, yaml_obj):
            priority_id = check_by_name(circle_object.priorities, yaml_obj["priority"]["name"])
            status_id = check_by_name(circle_object.issue_statuses, yaml_obj["status"]["name"])
            type_id = check_by_name(circle_object.issue_types, yaml_obj["type"]["name"])
            severity_id = check_by_name(circle_object.severities, yaml_obj["severity"]["name"])
            issue = circle_object.add_issue(
                yaml_obj["basic_info"]["subject"],
                priority_id,
                status_id,
                type_id,
                severity_id,
                description=yaml_obj["basic_info"].get("description"),
            )
            for field in yaml_obj["custom_fields"]:
                for attr in circle_object.list_issue_attributes():
                    if attr.name == field["name"]:
                        issue.set_attribute(attr.id, field["value"])
                        break
            return issue
        def import_tasks(circle_object, story_object, yaml_obj):
            status_id = check_by_name(circle_object.task_statuses, yaml_obj["status"]["name"])
            task = story_object.add_task(
                yaml_obj["basic_info"].get("subject"),
                status_id,
                description=yaml_obj["basic_info"].get("description", ""),
                tags=yaml_obj["basic_info"].get("tags"),
            )
            return task
        # Folders Path
        projects_path = j.sals.fs.join_paths(import_dir, "projects")
        stories_path = j.sals.fs.join_paths(import_dir, "stories")
        issues_path = j.sals.fs.join_paths(import_dir, "issues")
        tasks_path = j.sals.fs.join_paths(import_dir, "tasks")
        # List of Files inside project Folder
        projects = j.sals.fs.os.listdir(projects_path)
        for project_file in projects:
            if project_file.endswith(".yaml") or project_file.endswith(".yml"):
                with open(j.sals.fs.join_paths(projects_path, project_file)) as pf:
                    circle_yaml = yaml.full_load(pf)
                    circle_obj = import_circle(self, circle_yaml)
                    j.logger.info(f"<Circle {circle_obj.id} Created>")
                    for story in circle_yaml["stories"]:
                        with open(j.sals.fs.join_paths(stories_path, f"{story}.yaml")) as sf:
                            story_yaml = yaml.full_load(sf)
                            story_obj = import_story(circle_obj, story_yaml)
                            j.logger.info(f"<Story {story_obj.id} Created in Circle {circle_obj.id}>")
                            for task in story_yaml["tasks"]:
                                with open(j.sals.fs.join_paths(tasks_path, f"{task}.yaml")) as tf:
                                    task_yaml = yaml.full_load(tf)
                                    task_obj = import_tasks(circle_obj, story_obj, task_yaml)
                                    j.logger.info(f"<Task {task_obj.id} Created in Story {story_obj.id}>")
                                    task_obj.update()
                            story_obj.update()
                    for issue in circle_yaml["issues"]:
                        with open(j.sals.fs.join_paths(issues_path, f"{issue}.yaml")) as isf:
                            issue_yaml = yaml.full_load(isf)
                            issue_obj = import_issue(circle_obj, issue_yaml)
                            j.logger.info(f"<Issue {issue_obj.id} Created in Circle {circle_obj.id}>")
                            issue_obj.update()
                    circle_obj.update()
                    j.logger.info(f"<Circle {circle_obj.id} Imported with All Stories and Issues")
Classes
class TaigaClient (*args, **kwargs)- 
A simple attribute-based namespace.
SimpleNamespace(**kwargs)
base class implementation for any class with fields which supports getting/setting raw data for any instance fields.
any instance can have an optional name and a parent.
class Person(Base): name = fields.String() age = fields.Float() p = Person(name="ahmed", age="19") print(p.name, p.age)Args
parent_:Base, optional- parent instance. Defaults to None.
 instance_name_:str, optional- instance name. Defaults to None.
 **values- any given field values to initiate the instance with
 
Expand source code
class TaigaClient(Client): def credential_updated(self, value): self._api = None host = fields.String(default="https://projects.threefold.me") username = fields.String(on_update=credential_updated) password = fields.Secret(on_update=credential_updated) token = fields.Secret(on_update=credential_updated) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._api = None self.text = "" def __hash__(self): return hash(str(self)) @property def api(self): if not self._api: api = TaigaAPI(host=self.host) if self.token: api.token = self.token else: if not self.username or not self.password: raise j.exceptions.Runtime("Token or username and password are required") api.auth(self.username, self.password) self._api = api return self._api @lru_cache(maxsize=2048) def _get_project(self, project_id): return self.api.projects.get(project_id) @lru_cache(maxsize=2048) def _get_milestone(self, milestone_id): if milestone_id: return self.api.milestones.get(milestone_id) @lru_cache(maxsize=2048) def _get_priority(self, priority_id): return self.api.priorities.get(priority_id) @lru_cache(maxsize=2048) def _get_assignee(self, assignee_id): return CircleUser(self, self.api.users.get(assignee_id)) _get_user_by_id = _get_assignee def _get_users_by_ids(self, ids=None): ids = ids or [] return [self._get_user_by_id(x) for x in ids] def _get_issues_by_ids(self, ids=None): ids = ids or [] return [self._get_issue_by_id(x) for x in ids] def _get_task_by_id(self, id): return self.api.tasks.get(id) @lru_cache(maxsize=2048) def _get_issue_status(self, status_id): return self.api.issue_statuses.get(status_id) @lru_cache(maxsize=2048) def _get_user_stories_status(self, status_id): return self.api.user_story_statuses.get(status_id) @lru_cache(maxsize=2048) def _get_task_status(self, status_id): return self.api.task_statuses.get(status_id) @lru_cache(maxsize=2048) def _get_user_id(self, username): user = self.api.users.list(username=username) if user: user = user[0] return user.id else: raise j.exceptions.Input(f"Couldn't find user with username: {username}") @lru_cache(maxsize=2048) def _get_user_by_name(self, username): theid = self._get_user_id(username) return self._get_user_by_id(theid) def get_issue_custom_fields(self, id): """Get Issue Custom fields Args: id (int): Issue id Returns: List: List of dictionaries {name: "custom field name", value: {values as dict}} """ issue = self.api.issues.get(id) issue_attributes = issue.get_attributes()["attributes_values"] project_attributes = self._get_project(issue.project).list_issue_attributes() custom_fields = [] for p_attr in project_attributes: for k, value in issue_attributes.items(): if p_attr.id == int(k): try: custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)}) except: custom_fields.append({"name": p_attr.name, "value": value}) break return custom_fields def get_story_custom_fields(self, id): """Get User_Story Custom fields Args: id (int): User_Story id Returns: List: List of dictionaries {name: "custom field name", value: {values as dict}} """ user_story = self.api.user_stories.get(id) user_story_attributes = user_story.get_attributes()["attributes_values"] project_attributes = self._get_project(user_story.project).list_user_story_attributes() custom_fields = [] for p_attr in project_attributes: for k, value in user_story_attributes.items(): if p_attr.id == int(k): try: custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)}) except: custom_fields.append({"name": p_attr.name, "value": value}) break return custom_fields def get_user_circles(self, username): """Get circles owned by user Args: username (str): Name of the user """ user_id = self._get_user_id(username) circles = self.api.projects.list(member=user_id) user_circles = [] for circle in circles: if circle.owner["id"] == user_id: user_circles.append(self._resolve_object(circle)) return user_circles def get_circles_issues(self, project_id): """Get all issues in a circle/project Args: project_id (int): id of the circle/project Raises: j.exceptions.NotFound: if couldn't find circle with specified id """ try: circle = self.api.projects.get(project_id) except TaigaRestException: raise j.exceptions.NotFound(f"Couldn't find project with id: {project_id}") circle_issues = [] for issue in circle.list_issues(): issue.project = self._get_project(issue.project) issue.milestone = self._get_milestone(issue.milestone) issue.priority = self._get_priority(issue.priority) issue.assignee = self._get_assignee(issue.assigned_to) issue.status = self._get_issue_status(issue.status) circle_issues.append(issue) return circle_issues def get_user_stories(self, username): """Get all stories of a user Args: username (str): Name of the user """ user_id = self._get_user_id(username) user_stories = self.api.user_stories.list(assigned_to=user_id) user_stories = [] for user_story in user_stories: # user_story.project = self._get_project(user_story.project) # user_story.milestone = self._get_milestone(user_story.milestone) user_story.status = self._get_user_stories_status(user_story.status) user_stories.append(user_story) return user_stories def get_user_tasks(self, username): """Get all tasks of a user Args: username (str): Name of the user """ user_id = self._get_user_id(username) user_tasks = self.api.tasks.list(assigned_to=user_id) user_tasks = [] for user_task in user_tasks: # user_task.project = self._get_project(user_task.project) # user_task.milestone = self._get_milestone(user_task.milestone) user_task.status = self._get_task_status(user_task.status) user_tasks.append(self._resolve_object(user_task)) return user_tasks def move_story_to_circle(self, story_id, project_id): """Moves a story to another circle/project Args: story_id (int): User story id project_id (int): circle/project id Raises: j.exceptions.NotFound: No user story with specified id found j.exceptions.NotFound: No project with specified id found j.exceptions.Runtime: [description] Returns: int: New id of the migrated user story """ def _get_project_status(project_statuses, status): for project_status in project_statuses: if project_status.name == status: return project_status.id try: user_story = self.api.user_stories.get(story_id) except TaigaRestException: raise j.exceptions.NotFound(f"Couldn't find user story with id: {story_id}") project_stories_statuses = self.api.user_story_statuses.list(project=project_id) status = self._get_user_stories_status(user_story.status) story_status_id = _get_project_status(project_stories_statuses, status) try: migrate_story = self.api.user_stories.create( project=project_id, subject=user_story.subject, assigned_to=user_story.assigned_to, milestone=user_story.milestone, status=story_status_id, tags=user_story.tags, ) except TaigaRestException: raise j.exceptions.NotFound(f"No project with id: {project_id} found") try: comments = self.api.history.user_story.get(story_id) comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"])) for comment in comments: migrate_story.add_comment(comment["comment_html"]) project_tasks_statuses = self.api.task_statuses.list(project=project_id) for task in user_story.list_tasks(): status = self._get_task_status(task.status) task_status_id = _get_project_status(project_tasks_statuses, status) migrate_task = migrate_story.add_task( subject=task.subject, status=task_status_id, due_date=task.due_date, milestone=task.milestone, assigned_to=task.assigned_to, tags=task.tags, project=migrate_story.project, user_story=migrate_story.id, ) comments = self.api.history.task.get(migrate_task.id) comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"])) for comment in comments: migrate_task.add_comment(comment["comment_html"]) except Exception as e: self.api.user_stories.delete(migrate_story.id) raise j.exceptions.Runtime(f"Failed to migrate story error was: {str(e)}") self.api.user_stories.delete(story_id) return migrate_story.id def list_all_issues(self, username="", full_info=False): """ List all issues for specific user if you didn't pass user_id will list all the issues HINT: Using full_info will take a longer time Args: username (str): username. full_info (bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Issue. """ if username: user_id = self._get_user_id(username) if not full_info: return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list(assigned_to=user_id)] else: return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list(assigned_to=user_id)] else: if not full_info: return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list()] else: return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list()] def list_all_tasks(self, username="", full_info=False): """ List all tasks for specific user if you didn't pass user_id will list all the tasks HINT: Using full_info will take a longer time Args: username (str): username. full_info (bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Task. """ if username: user_id = self._get_user_id(username) if not full_info: return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list(assigned_to=user_id)] else: return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list(assigned_to=user_id)] else: if not full_info: return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list()] else: return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list()] def list_all_projects(self, full_info=False): """ List all projects HINT: Using full_info will take a longer time Args: full_info(bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Project. """ if not full_info: return [Circle(self, self._resolve_object(x)) for x in self.api.projects.list()] else: return [Circle(self, self.api.projects.get(x.id)) for x in self.api.projects.list()] def list_all_active_projects(self, full_info=False): """ List all projects not starting with "ARHCIVE" HINT: Using full_info will take a longer time Args: full_info (bool): [description]. Defaults to False. Returns: [type]: [description] """ return [ Circle(self, p) for p in self.list_projects_by(lambda x: not x.name.startswith("ARCHIVE_"), full_info=full_info) ] def list_all_milestones(self): """ List all milestones Returns: List: List of taiga.models.models.Milestone. """ return [self._resolve_object(x) for x in self.api.milestones.list()] def list_all_user_stories(self, username="", full_info=False): """ List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time Args: username (str): username. full_info(bool): flag used to get object with full info. Defaults to False Returns: List: List of CircleStory. """ if username: user_id = self._get_user_id(username) if not full_info: return [ CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list(assigned_to=user_id) ] else: return [ CircleStory(self, self.api.user_stories.get(x.id)) for x in self.api.user_stories.list(assigned_to=user_id) ] else: if not full_info: return [CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list()] else: return [CircleStory(self, self.api.user_stories.get(x.id)) for x in self.api.user_stories.list()] def list_all_users(self, full_info=False): """ List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time Args: username (str): username. full_info(bool): flag used to get object with full info. Defaults to False Returns: List: List of CircleUser. """ circles = self.list_all_projects() users = set() for c in circles: for m in c.members: users.add(m) return [CircleUser(self, self._get_user_by_id(uid)) for uid in users] def get_issue_by_id(self, issue_id): """Get issue Args: issue_id: the id of the desired issue Returns: Issue object: issue """ return CircleIssue(self, self.api.issues.get(issue_id)) def _resolve_object(self, obj): resolvers = { "owners": self._get_users_by_ids, "watchers": self._get_users_by_ids, "members": self._get_users_by_ids, "project": self._get_project, "circle": self._get_project, "milestone": self._get_milestone, "task_status": self._get_task_status, "assigned_to": self._get_user_by_id, "owner": self._get_user_by_id, "issues": self._get_issues_by_ids, "tasks": self._get_task_by_id, } newobj = copy.deepcopy(obj) for k in dir(newobj): v = getattr(newobj, k) if isinstance(v, int) or isinstance(v, list) and v and isinstance(v[0], int): if k in resolvers: resolved = None resolver = resolvers[k] try: copied_v = copy.deepcopy(v) resolved = lambda: resolver(copied_v) if isinstance(v, list): setattr(newobj, f"{k}_objects", resolved) else: setattr(newobj, f"{k}_object", resolved) except Exception as e: import traceback traceback.print_exc() j.logger.error(f"error {e}") return newobj def list_projects_by(self, fn=lambda x: True, full_info=False): return [p for p in self.list_all_projects(full_info=full_info) if fn(p)] def list_team_circles(self): return [TeamCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("TEAM_"))] def list_project_circles(self): return [ProjectCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("PROJECT_"))] def list_funnel_circles(self): return [FunnelCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("FUNNEL_"))] def validate_custom_fields(self, attributes): """Validate custom fields values to match our requirements Args: attributes (List): Output from get_issue/story_custom_fields functions Raises: j.exceptions.Validation: Raise validation exception if any input not valid Returns: bool: Return True if no exception raised and print logs """ for attr in attributes: name = attr.get("name") value = attr.get("value") period = value.get("period", "onetime") duration = value.get("duration", 1) amount = value.get("amount", 0) currency = value.get("currency", "eur") start_date = value.get("start_date", f"{dateutil.utils.today().month}:{dateutil.utils.today().year}",) confidence = value.get("confidence", 100) user = value.get("user") part = value.get("part", "0%") type = value.get("type", "revenue") if name not in ["bookings", "commission"]: raise j.exceptions.Validation( f'Name: ({name}) is unknown custom field, please select one of the following ["bookings", "commission"]' ) if period not in ["onetime", "month", "year"]: raise j.exceptions.Validation( f'Period: ({period}) not found, please select one of following ["onetime", "month", "year"]' ) if duration < 1 or duration > 120: raise j.exceptions.Validation(f"Duration: ({duration}) is not in range, please select it from 1 to 120") if not isinstance(amount, int): raise j.exceptions.Validation(f"Amount: ({amount}) is not integer, please add int value") if currency.replace(" ", "").lower() not in [ "usd", "chf", "eur", "gbp", "egp", ]: raise j.exceptions.Validation( f'Currency: ({currency}) is not supported, please use one of the following currencies ["usd", "chf", "eur", "gbp", "egp"]' ) try: date = start_date.split(":") month = int(date[0]) year = int(date[1]) if len(date) > 1 else dateutil.utils.today().year if month < 1 or month > 12: raise j.exceptions.Validation( "Please use values from 1 to 12 in Month field, follow format like MONTH:YEAR as 11:2020 or MONTH as 11" ) except ValueError as e: raise j.exceptions.Validation( "Please use numeric date with the following format MONTH:YEAR as 11:2020 or MONTH as 11" ) except AttributeError as e: pass # Will check what happen if start_date not provide if confidence % 10 != 0: j.exceptions.Validation(f"Confidence: ({confidence}) not multiple of 10, it must be multiple of 10") part_tmp = part.replace("%", "") if user != None and user not in self.list_all_users(): raise j.exceptions.Validation(f"User: ({user}) is not found") if int(part_tmp) < 0 or int(part_tmp) > 100: j.exceptions.Validation(f"Part: ({part}) is a not a valid percentage, it must be from 0% to 100%") if type not in ["revenue", "booking"]: raise j.exceptions.Validation( f'Type: ({type}) is not supported type, please choose one of the following ["revenue" , "booking"]' ) j.logger.info(f"Attribute: {name} passed") return True def _create_new_circle( self, name, type_="team", description="desc", severities=None, issues_statuses=None, priorities=None, issues_types=None, user_stories_statuses=None, tasks_statuses=None, custom_fields=None, **attrs, ): severities = severities or ["Low", "Mid", "High"] priorities = priorities or [ "Wishlist", "Minor", "Normal", "Important", "Critical", ] issues_statuses = issues_statuses or [ "New", "In progress", "Ready for test", "Closed", "Needs Info", "Rejected", "Postponed", ] issues_types = issues_types or [] user_stories_statuses = user_stories_statuses or [] tasks_statuses = tasks_statuses or [] custom_fields = custom_fields or [] type_ = type_.upper() project_name = f"{type_}_{name}" p = self.api.projects.create(project_name, description=description) for t in tasks_statuses: try: p.add_task_status(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping task {t} {e}") for t in priorities: try: p.add_priority(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping prio {t} {e}") for t in severities: try: p.add_severity(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping sever {t} {e}") for t in issues_statuses: try: p.add_issue_status(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping status {t} {e}") for t in user_stories_statuses: try: p.add_user_story_status(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping user status {t} {e}") for t in issues_types: try: p.add_issue_type(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping issue type {t} {e}") for t in custom_fields: try: p.add_issue_attribute(t) p.add_user_story_attribute(t) except Exception as e: # check if duplicated j.logger.debug(f"skipping custom field type {t} {e}") return p def create_new_project_circle( self, name, description="", **attrs, ): """Creates a new project circle. Args: name (str): circle name description (str, optional): circle description. Defaults to "". Returns: [ProjectCircle]: Project circle """ attrs = { "is_backlog_activated": False, "is_issues_activated": True, "is_kanban_activated": True, "is_private": False, "is_wiki_activated": True, } issues_types = ["Bug", "Question", "Enhancement"] severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"] priorities = None story_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Verified", "Archived", ] item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"] issues_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Closed", "Rejected", "Postponed", "Archived", ] return ProjectCircle( self, self._create_new_circle( name, type_="project", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=item_statuses, **attrs, ), ) def create_new_team_circle(self, name, description="", **attrs): """Creates a new team circle. using sprints & timeline (does not use kanban) Args: name (str): circle name description (str, optional): circle description. Defaults to "". severities (List[str], optional): list of strings to represent severities. Defaults to None. issues_statuses (List[str], optional): list of strings to represent issues_stauses. Defaults to None. priorities (List[str], optional): list of strings to represent priorities. Defaults to None. issues_types (List[str], optional): list of strings to represent issues types. Defaults to None. user_stories_statuses (List[str], optional): list of strings to represent user stories. Defaults to None. tasks_statuses (List[str], optional): list of strings to represent task statuses. Defaults to None. Returns: [TeamCircle]: team circle """ attrs = { "is_backlog_activated": True, "is_issues_activated": True, "is_kanban_activated": False, "is_private": False, "is_wiki_activated": True, } issues_types = ["Bug", "Question", "Enhancement"] severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"] priorities = None story_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Verified", "Archived", ] item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"] issues_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Closed", "Rejected", "Postponed", "Archived", ] return TeamCircle( self, self._create_new_circle( name, type_="team", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=item_statuses, **attrs, ), ) def create_new_funnel_circle(self, name, description="", **attrs): """Creates a new funnel circle. using sprints & timeline (does not use kanban) Args: name (str): circle name description (str, optional): circle description. Defaults to "". Returns: [FunnelCircle]: funnel circle """ attrs = { "is_backlog_activated": False, "is_issues_activated": True, "is_kanban_activated": True, "is_private": False, "is_wiki_activated": True, } severities = ["unknown", "low", "25%", "50%", "75%", "90%"] priorities = ["Low", "Normal", "High"] issues_types = "opportunity" issues_statuses = [ "New", "Interested", "Deal", "Blocked", "NeedInfo", "Lost", "Postponed", "Won", ] story_statuses = [ "New", "Proposal", "Contract", "Blocked", "NeedInfo", "Closed", ] task_statuses = ["New", "In progress", "Verification", "Needs info", "Closed"] custom_fields = ["bookings", "commission"] return FunnelCircle( self, self._create_new_circle( name, type_="funnel", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=task_statuses, custom_fields=custom_fields, **attrs, ), ) def export_circles_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False): """export circles into {wikipath}/src/circles HINT: Using full_info will take longer time Args: wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki". full_info (bool): export object with full info. Defaults to False """ path = j.sals.fs.join_paths(wikipath, "src", "circles") j.sals.fs.mkdirs(path) circles = self.list_all_active_projects(full_info=full_info) def write_md_for_circle(circle): circle_md = circle.as_md circle_mdpath = j.sals.fs.join_paths(path, f"{circle.clean_name}.md") if not ( modified_only and j.sals.fs.exists(circle_mdpath) and j.sals.fs.read_ascii(circle_mdpath) == circle_md ): j.sals.fs.write_ascii(circle_mdpath, circle_md) circles_mdpath = j.sals.fs.join_paths(path, "circles.md") circles_mdcontent = "# circles\n\n" for c in circles: circles_mdcontent += f"- [{c.name}](./{c.clean_name}.md)\n" j.sals.fs.write_ascii(circles_mdpath, circles_mdcontent) greenlets = [gevent.spawn(write_md_for_circle, gcircle_obj) for gcircle_obj in circles] gevent.joinall(greenlets) def export_users_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False): """export users into {wikipath}/src/users HINT: Using full_info will take longer time Args: wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki". modified_only (bool): export moidified objects only full_info (bool): export object with full info. Defaults to False """ path = j.sals.fs.join_paths(wikipath, "src", "users") j.sals.fs.mkdirs(path) users_objects = self.list_all_users(full_info=full_info) users_mdpath = j.sals.fs.join_paths(path, "users.md") users_mdcontent = "# users\n\n" def write_md_for_user(user): user_md = user.as_md user_mdpath = j.sals.fs.join_paths(path, f"{user.clean_name}.md") if not (modified_only and j.sals.fs.exists(user_mdpath) and j.sals.fs.read_ascii(user_mdpath) == user_md): j.sals.fs.write_ascii(user_mdpath, user_md) for u in users_objects: users_mdcontent += f"- [{u.username}](./{u.clean_name}.md)\n" j.sals.fs.write_ascii(users_mdpath, users_mdcontent) greenlets = [gevent.spawn(write_md_for_user, guser_obj) for guser_obj in users_objects] gevent.joinall(greenlets) def export_as_md(self, wiki_path="/tmp/taigawiki", modified_only: bool = True, full_info=False): """export taiga instance into a wiki showing users and circles HINT: Using full_info will take longer time Args: wiki_src_path (str, optional): wiki path. Defaults to "/tmp/taigawiki". modified_only (bool): write modified objects only. Defaults to True full_info (bool): export object with full info. Defaults to False """ j.logger.info("Start Exporting Wiki ...") gs = [] gs.append(gevent.spawn(self.export_circles_as_md, wiki_path, modified_only, full_info)) gs.append(gevent.spawn(self.export_users_as_md, wiki_path, modified_only, full_info)) gevent.joinall(gs) template_file = j.sals.fs.join_paths(Path(__file__).parent, "template.html") index_html_path = j.sals.fs.join_paths(wiki_path, "src", "index.html") readme_md_path = j.sals.fs.join_paths(wiki_path, "src", "README.md") sidebar_md_path = j.sals.fs.join_paths(wiki_path, "src", "_sidebar.md") content = dedent( f""" # Taiga overview - [circles](./circles/circles.md) - [users](./users/users.md) """ ) j.sals.fs.write_ascii(readme_md_path, content) j.sals.fs.write_ascii(sidebar_md_path, content) j.sals.fs.copy_file(template_file, index_html_path) j.logger.info(f"Exported at {wiki_path}") def export_as_md_periodically( self, wiki_path="/tmp/taigawiki", period: int = 300, modified_only: bool = True, full_info=False ): """export taiga instance into a wiki showing users and circles periodically HINT: Using full_info will take longer time Args: wiki_path (str, optional): wiki path. Defaults to "/tmp/taigawiki". period (int): Time to wait between each export in "Seconds". Defaults to 300 (5 Min). modified_only (bool): write modified objects only.. Defaults to True. full_info (bool): export object with full info. Defaults to False """ repeater = Event() while True: j.logger.info("Start Exporting ....") self.export_as_md(wiki_path, modified_only, full_info) j.logger.info(f"Exported at {wiki_path}") repeater.wait(period) def export_as_yaml(self, export_dir="/tmp/export_dir", full_info=False): """export taiga instance [Circle, Story, Issue, Task , User] into a yaml files HINT: Using full_info will take longer time Args: export_dir (str, optional): [description]. Defaults to "/tmp/export_dir". full_info (bool): export object with full info. Defaults to False """ def _export_objects_to_dir(objects_dir, objects_fun, full_info): j.sals.fs.mkdirs(objects_dir) objects = objects_fun(full_info=full_info) for obj in objects: try: outpath = j.sals.fs.join_paths(objects_dir, f"{obj.id}.yaml") j.sals.fs.write_ascii(outpath, obj.as_yaml) except Exception as e: import traceback traceback.print_exc() j.logger.error(e) j.logger.error(f"{type(obj)}: {obj.id}") projects_path = j.sals.fs.join_paths(export_dir, "projects") stories_path = j.sals.fs.join_paths(export_dir, "stories") issues_path = j.sals.fs.join_paths(export_dir, "issues") tasks_path = j.sals.fs.join_paths(export_dir, "tasks") # Milestones is not one of our model objects # milestones_path = j.sals.fs.join_paths(export_dir, "milestones") users_path = j.sals.fs.join_paths(export_dir, "users") def on_err(*args, **kwargs): print("err, ", args, kwargs) j.logger.info("Start Export as YAML") gs = [] gs.append(gevent.spawn(_export_objects_to_dir, projects_path, self.list_all_active_projects, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, stories_path, self.list_all_user_stories, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, issues_path, self.list_all_issues, full_info)) # Milestones is not one of our model objects # gs.append(gevent.spawn(_export_objects_to_dir, milestones_path, self.list_all_milestones) gs.append(gevent.spawn(_export_objects_to_dir, users_path, self.list_all_users, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, tasks_path, self.list_all_tasks, full_info)) gevent.joinall(gs) j.logger.info("Finish Export as YAML") def import_from_yaml(self, import_dir="/tmp/export_dir"): """Import Circle with all stories, issues and tasks from yaml files Args: import_dir (str): import directory path. Defaults to "/tmp/export_dir". """ # Helper Functions def check_by_name(list_obj, name_to_find): for obj in list_obj: if obj.name == name_to_find: return obj.id return list_obj[0].id def import_circle(self, yaml_obj): circle = None # Funnel Circle if yaml_obj["basic_info"]["name"].lower() == "funnel": circle = self.create_new_funnel_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Team Circle elif yaml_obj["basic_info"]["name"].lower() == "team": circle = self.create_new_team_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Project Circle elif yaml_obj["basic_info"]["name"].lower() == "project": circle = self.create_new_project_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Any Other Circle else: circle = self._create_new_circle(yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],) circle.is_backlog_activated = yaml_obj["modules"]["is_backlog_activated"] circle.is_issues_activated = yaml_obj["modules"]["is_issues_activated"] circle.is_kanban_activated = yaml_obj["modules"]["is_kanban_activated"] circle.is_wiki_activated = yaml_obj["modules"]["is_wiki_activated"] circle.is_private = yaml_obj["basic_info"]["is_private"] circle.videoconferences = yaml_obj["modules"]["videoconferences"] for issue_attr in yaml_obj["issues_attributes"]: circle.add_issue_attribute(issue_attr) for us_attr in yaml_obj["stories_attributes"]: circle.add_user_story_attribute(us_attr) return circle def import_story(circle_object, yaml_obj): status_id = check_by_name(circle_object.us_statuses, yaml_obj["status"]["name"]) story = circle_object.add_user_story( yaml_obj["basic_info"].get("subject"), tags=yaml_obj["basic_info"].get("tags"), description=yaml_obj["basic_info"].get("description", ""), client_requirement=yaml_obj["requirements"].get("client_requirement"), team_requirement=yaml_obj["requirements"].get("team_requirement"), is_blocked=yaml_obj["additional_info"].get("is_blocked"), due_date=yaml_obj["date"].get("due_date"), status=status_id, ) for field in yaml_obj.get("custom_fields", []): for attr in circle_object.list_user_story_attributes(): if attr.name == field["name"]: story.set_attribute(attr.id, field["value"]) break return story def import_issue(circle_object, yaml_obj): priority_id = check_by_name(circle_object.priorities, yaml_obj["priority"]["name"]) status_id = check_by_name(circle_object.issue_statuses, yaml_obj["status"]["name"]) type_id = check_by_name(circle_object.issue_types, yaml_obj["type"]["name"]) severity_id = check_by_name(circle_object.severities, yaml_obj["severity"]["name"]) issue = circle_object.add_issue( yaml_obj["basic_info"]["subject"], priority_id, status_id, type_id, severity_id, description=yaml_obj["basic_info"].get("description"), ) for field in yaml_obj["custom_fields"]: for attr in circle_object.list_issue_attributes(): if attr.name == field["name"]: issue.set_attribute(attr.id, field["value"]) break return issue def import_tasks(circle_object, story_object, yaml_obj): status_id = check_by_name(circle_object.task_statuses, yaml_obj["status"]["name"]) task = story_object.add_task( yaml_obj["basic_info"].get("subject"), status_id, description=yaml_obj["basic_info"].get("description", ""), tags=yaml_obj["basic_info"].get("tags"), ) return task # Folders Path projects_path = j.sals.fs.join_paths(import_dir, "projects") stories_path = j.sals.fs.join_paths(import_dir, "stories") issues_path = j.sals.fs.join_paths(import_dir, "issues") tasks_path = j.sals.fs.join_paths(import_dir, "tasks") # List of Files inside project Folder projects = j.sals.fs.os.listdir(projects_path) for project_file in projects: if project_file.endswith(".yaml") or project_file.endswith(".yml"): with open(j.sals.fs.join_paths(projects_path, project_file)) as pf: circle_yaml = yaml.full_load(pf) circle_obj = import_circle(self, circle_yaml) j.logger.info(f"<Circle {circle_obj.id} Created>") for story in circle_yaml["stories"]: with open(j.sals.fs.join_paths(stories_path, f"{story}.yaml")) as sf: story_yaml = yaml.full_load(sf) story_obj = import_story(circle_obj, story_yaml) j.logger.info(f"<Story {story_obj.id} Created in Circle {circle_obj.id}>") for task in story_yaml["tasks"]: with open(j.sals.fs.join_paths(tasks_path, f"{task}.yaml")) as tf: task_yaml = yaml.full_load(tf) task_obj = import_tasks(circle_obj, story_obj, task_yaml) j.logger.info(f"<Task {task_obj.id} Created in Story {story_obj.id}>") task_obj.update() story_obj.update() for issue in circle_yaml["issues"]: with open(j.sals.fs.join_paths(issues_path, f"{issue}.yaml")) as isf: issue_yaml = yaml.full_load(isf) issue_obj = import_issue(circle_obj, issue_yaml) j.logger.info(f"<Issue {issue_obj.id} Created in Circle {circle_obj.id}>") issue_obj.update() circle_obj.update() j.logger.info(f"<Circle {circle_obj.id} Imported with All Stories and Issues")Ancestors
Instance variables
var api- 
Expand source code
@property def api(self): if not self._api: api = TaigaAPI(host=self.host) if self.token: api.token = self.token else: if not self.username or not self.password: raise j.exceptions.Runtime("Token or username and password are required") api.auth(self.username, self.password) self._api = api return self._api var host- 
getter method this property
will call
_get_value, which would if the value is already defined and will get the default value if notReturns
any- the field value
 
Expand source code
def getter(self): """ getter method this property will call `_get_value`, which would if the value is already defined and will get the default value if not Returns: any: the field value """ return self._get_value(name, field) var password- 
getter method this property
will call
_get_value, which would if the value is already defined and will get the default value if notReturns
any- the field value
 
Expand source code
def getter(self): """ getter method this property will call `_get_value`, which would if the value is already defined and will get the default value if not Returns: any: the field value """ return self._get_value(name, field) var token- 
getter method this property
will call
_get_value, which would if the value is already defined and will get the default value if notReturns
any- the field value
 
Expand source code
def getter(self): """ getter method this property will call `_get_value`, which would if the value is already defined and will get the default value if not Returns: any: the field value """ return self._get_value(name, field) var username- 
getter method this property
will call
_get_value, which would if the value is already defined and will get the default value if notReturns
any- the field value
 
Expand source code
def getter(self): """ getter method this property will call `_get_value`, which would if the value is already defined and will get the default value if not Returns: any: the field value """ return self._get_value(name, field) 
Methods
def create_new_funnel_circle(self, name, description='', **attrs)- 
Creates a new funnel circle. using sprints & timeline (does not use kanban)
Args
name:str- circle name
 description:str, optional- circle description. Defaults to "".
 
Returns
[FunnelCircle]- funnel circle
 
Expand source code
def create_new_funnel_circle(self, name, description="", **attrs): """Creates a new funnel circle. using sprints & timeline (does not use kanban) Args: name (str): circle name description (str, optional): circle description. Defaults to "". Returns: [FunnelCircle]: funnel circle """ attrs = { "is_backlog_activated": False, "is_issues_activated": True, "is_kanban_activated": True, "is_private": False, "is_wiki_activated": True, } severities = ["unknown", "low", "25%", "50%", "75%", "90%"] priorities = ["Low", "Normal", "High"] issues_types = "opportunity" issues_statuses = [ "New", "Interested", "Deal", "Blocked", "NeedInfo", "Lost", "Postponed", "Won", ] story_statuses = [ "New", "Proposal", "Contract", "Blocked", "NeedInfo", "Closed", ] task_statuses = ["New", "In progress", "Verification", "Needs info", "Closed"] custom_fields = ["bookings", "commission"] return FunnelCircle( self, self._create_new_circle( name, type_="funnel", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=task_statuses, custom_fields=custom_fields, **attrs, ), ) def create_new_project_circle(self, name, description='', **attrs)- 
Creates a new project circle.
Args
name:str- circle name
 description:str, optional- circle description. Defaults to "".
 
Returns
[ProjectCircle]- Project circle
 
Expand source code
def create_new_project_circle( self, name, description="", **attrs, ): """Creates a new project circle. Args: name (str): circle name description (str, optional): circle description. Defaults to "". Returns: [ProjectCircle]: Project circle """ attrs = { "is_backlog_activated": False, "is_issues_activated": True, "is_kanban_activated": True, "is_private": False, "is_wiki_activated": True, } issues_types = ["Bug", "Question", "Enhancement"] severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"] priorities = None story_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Verified", "Archived", ] item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"] issues_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Closed", "Rejected", "Postponed", "Archived", ] return ProjectCircle( self, self._create_new_circle( name, type_="project", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=item_statuses, **attrs, ), ) def create_new_team_circle(self, name, description='', **attrs)- 
Creates a new team circle. using sprints & timeline (does not use kanban)
Args
name:str- circle name
 description:str, optional- circle description. Defaults to "".
 severities:List[str], optional- list of strings to represent severities. Defaults to None.
 issues_statuses:List[str], optional- list of strings to represent issues_stauses. Defaults to None.
 priorities:List[str], optional- list of strings to represent priorities. Defaults to None.
 issues_types:List[str], optional- list of strings to represent issues types. Defaults to None.
 user_stories_statuses:List[str], optional- list of strings to represent user stories. Defaults to None.
 tasks_statuses:List[str], optional- list of strings to represent task statuses. Defaults to None.
 
Returns
[TeamCircle]- team circle
 
Expand source code
def create_new_team_circle(self, name, description="", **attrs): """Creates a new team circle. using sprints & timeline (does not use kanban) Args: name (str): circle name description (str, optional): circle description. Defaults to "". severities (List[str], optional): list of strings to represent severities. Defaults to None. issues_statuses (List[str], optional): list of strings to represent issues_stauses. Defaults to None. priorities (List[str], optional): list of strings to represent priorities. Defaults to None. issues_types (List[str], optional): list of strings to represent issues types. Defaults to None. user_stories_statuses (List[str], optional): list of strings to represent user stories. Defaults to None. tasks_statuses (List[str], optional): list of strings to represent task statuses. Defaults to None. Returns: [TeamCircle]: team circle """ attrs = { "is_backlog_activated": True, "is_issues_activated": True, "is_kanban_activated": False, "is_private": False, "is_wiki_activated": True, } issues_types = ["Bug", "Question", "Enhancement"] severities = ["Wishlist", "Minor", "Normal", "Important", "Critical"] priorities = None story_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Verified", "Archived", ] item_statuses = ["New", "to-start", "in-progress", "Blocked", "Done"] issues_statuses = [ "New", "to-start", "in-progress", "Blocked", "Implemented", "Closed", "Rejected", "Postponed", "Archived", ] return TeamCircle( self, self._create_new_circle( name, type_="team", description=description, severities=severities, issues_statuses=issues_statuses, priorities=priorities, issues_types=issues_types, user_stories_statuses=story_statuses, tasks_statuses=item_statuses, **attrs, ), ) def credential_updated(self, value)- 
Expand source code
def credential_updated(self, value): self._api = None def export_as_md(self, wiki_path='/tmp/taigawiki', modified_only: bool = True, full_info=False)- 
export taiga instance into a wiki showing users and circles HINT: Using full_info will take longer time
Args
wiki_src_path:str, optional- wiki path. Defaults to "/tmp/taigawiki".
 modified_only:bool- write modified objects only. Defaults to True
 full_info:bool- export object with full info. Defaults to False
 
Expand source code
def export_as_md(self, wiki_path="/tmp/taigawiki", modified_only: bool = True, full_info=False): """export taiga instance into a wiki showing users and circles HINT: Using full_info will take longer time Args: wiki_src_path (str, optional): wiki path. Defaults to "/tmp/taigawiki". modified_only (bool): write modified objects only. Defaults to True full_info (bool): export object with full info. Defaults to False """ j.logger.info("Start Exporting Wiki ...") gs = [] gs.append(gevent.spawn(self.export_circles_as_md, wiki_path, modified_only, full_info)) gs.append(gevent.spawn(self.export_users_as_md, wiki_path, modified_only, full_info)) gevent.joinall(gs) template_file = j.sals.fs.join_paths(Path(__file__).parent, "template.html") index_html_path = j.sals.fs.join_paths(wiki_path, "src", "index.html") readme_md_path = j.sals.fs.join_paths(wiki_path, "src", "README.md") sidebar_md_path = j.sals.fs.join_paths(wiki_path, "src", "_sidebar.md") content = dedent( f""" # Taiga overview - [circles](./circles/circles.md) - [users](./users/users.md) """ ) j.sals.fs.write_ascii(readme_md_path, content) j.sals.fs.write_ascii(sidebar_md_path, content) j.sals.fs.copy_file(template_file, index_html_path) j.logger.info(f"Exported at {wiki_path}") def export_as_md_periodically(self, wiki_path='/tmp/taigawiki', period: int = 300, modified_only: bool = True, full_info=False)- 
export taiga instance into a wiki showing users and circles periodically HINT: Using full_info will take longer time
Args
wiki_path:str, optional- wiki path. Defaults to "/tmp/taigawiki".
 period:int- Time to wait between each export in "Seconds". Defaults to 300 (5 Min).
 modified_only:bool- write modified objects only.. Defaults to True.
 full_info:bool- export object with full info. Defaults to False
 
Expand source code
def export_as_md_periodically( self, wiki_path="/tmp/taigawiki", period: int = 300, modified_only: bool = True, full_info=False ): """export taiga instance into a wiki showing users and circles periodically HINT: Using full_info will take longer time Args: wiki_path (str, optional): wiki path. Defaults to "/tmp/taigawiki". period (int): Time to wait between each export in "Seconds". Defaults to 300 (5 Min). modified_only (bool): write modified objects only.. Defaults to True. full_info (bool): export object with full info. Defaults to False """ repeater = Event() while True: j.logger.info("Start Exporting ....") self.export_as_md(wiki_path, modified_only, full_info) j.logger.info(f"Exported at {wiki_path}") repeater.wait(period) def export_as_yaml(self, export_dir='/tmp/export_dir', full_info=False)- 
export taiga instance [Circle, Story, Issue, Task , User] into a yaml files HINT: Using full_info will take longer time
Args
export_dir:str, optional- [description]. Defaults to "/tmp/export_dir".
 full_info:bool- export object with full info. Defaults to False
 
Expand source code
def export_as_yaml(self, export_dir="/tmp/export_dir", full_info=False): """export taiga instance [Circle, Story, Issue, Task , User] into a yaml files HINT: Using full_info will take longer time Args: export_dir (str, optional): [description]. Defaults to "/tmp/export_dir". full_info (bool): export object with full info. Defaults to False """ def _export_objects_to_dir(objects_dir, objects_fun, full_info): j.sals.fs.mkdirs(objects_dir) objects = objects_fun(full_info=full_info) for obj in objects: try: outpath = j.sals.fs.join_paths(objects_dir, f"{obj.id}.yaml") j.sals.fs.write_ascii(outpath, obj.as_yaml) except Exception as e: import traceback traceback.print_exc() j.logger.error(e) j.logger.error(f"{type(obj)}: {obj.id}") projects_path = j.sals.fs.join_paths(export_dir, "projects") stories_path = j.sals.fs.join_paths(export_dir, "stories") issues_path = j.sals.fs.join_paths(export_dir, "issues") tasks_path = j.sals.fs.join_paths(export_dir, "tasks") # Milestones is not one of our model objects # milestones_path = j.sals.fs.join_paths(export_dir, "milestones") users_path = j.sals.fs.join_paths(export_dir, "users") def on_err(*args, **kwargs): print("err, ", args, kwargs) j.logger.info("Start Export as YAML") gs = [] gs.append(gevent.spawn(_export_objects_to_dir, projects_path, self.list_all_active_projects, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, stories_path, self.list_all_user_stories, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, issues_path, self.list_all_issues, full_info)) # Milestones is not one of our model objects # gs.append(gevent.spawn(_export_objects_to_dir, milestones_path, self.list_all_milestones) gs.append(gevent.spawn(_export_objects_to_dir, users_path, self.list_all_users, full_info)) gs.append(gevent.spawn(_export_objects_to_dir, tasks_path, self.list_all_tasks, full_info)) gevent.joinall(gs) j.logger.info("Finish Export as YAML") def export_circles_as_md(self, wikipath='/tmp/taigawiki', modified_only=True, full_info=False)- 
export circles into {wikipath}/src/circles HINT: Using full_info will take longer time
Args
wikipath:str, optional- wiki path. Defaults to "/tmp/taigawiki".
 full_info:bool- export object with full info. Defaults to False
 
Expand source code
def export_circles_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False): """export circles into {wikipath}/src/circles HINT: Using full_info will take longer time Args: wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki". full_info (bool): export object with full info. Defaults to False """ path = j.sals.fs.join_paths(wikipath, "src", "circles") j.sals.fs.mkdirs(path) circles = self.list_all_active_projects(full_info=full_info) def write_md_for_circle(circle): circle_md = circle.as_md circle_mdpath = j.sals.fs.join_paths(path, f"{circle.clean_name}.md") if not ( modified_only and j.sals.fs.exists(circle_mdpath) and j.sals.fs.read_ascii(circle_mdpath) == circle_md ): j.sals.fs.write_ascii(circle_mdpath, circle_md) circles_mdpath = j.sals.fs.join_paths(path, "circles.md") circles_mdcontent = "# circles\n\n" for c in circles: circles_mdcontent += f"- [{c.name}](./{c.clean_name}.md)\n" j.sals.fs.write_ascii(circles_mdpath, circles_mdcontent) greenlets = [gevent.spawn(write_md_for_circle, gcircle_obj) for gcircle_obj in circles] gevent.joinall(greenlets) def export_users_as_md(self, wikipath='/tmp/taigawiki', modified_only=True, full_info=False)- 
export users into {wikipath}/src/users HINT: Using full_info will take longer time
Args
wikipath:str, optional- wiki path. Defaults to "/tmp/taigawiki".
 modified_only:bool- export moidified objects only
 full_info:bool- export object with full info. Defaults to False
 
Expand source code
def export_users_as_md(self, wikipath="/tmp/taigawiki", modified_only=True, full_info=False): """export users into {wikipath}/src/users HINT: Using full_info will take longer time Args: wikipath (str, optional): wiki path. Defaults to "/tmp/taigawiki". modified_only (bool): export moidified objects only full_info (bool): export object with full info. Defaults to False """ path = j.sals.fs.join_paths(wikipath, "src", "users") j.sals.fs.mkdirs(path) users_objects = self.list_all_users(full_info=full_info) users_mdpath = j.sals.fs.join_paths(path, "users.md") users_mdcontent = "# users\n\n" def write_md_for_user(user): user_md = user.as_md user_mdpath = j.sals.fs.join_paths(path, f"{user.clean_name}.md") if not (modified_only and j.sals.fs.exists(user_mdpath) and j.sals.fs.read_ascii(user_mdpath) == user_md): j.sals.fs.write_ascii(user_mdpath, user_md) for u in users_objects: users_mdcontent += f"- [{u.username}](./{u.clean_name}.md)\n" j.sals.fs.write_ascii(users_mdpath, users_mdcontent) greenlets = [gevent.spawn(write_md_for_user, guser_obj) for guser_obj in users_objects] gevent.joinall(greenlets) def get_circles_issues(self, project_id)- 
Get all issues in a circle/project
Args
project_id:int- id of the circle/project
 
Raises
j.exceptions.NotFound- if couldn't find circle with specified id
 
Expand source code
def get_circles_issues(self, project_id): """Get all issues in a circle/project Args: project_id (int): id of the circle/project Raises: j.exceptions.NotFound: if couldn't find circle with specified id """ try: circle = self.api.projects.get(project_id) except TaigaRestException: raise j.exceptions.NotFound(f"Couldn't find project with id: {project_id}") circle_issues = [] for issue in circle.list_issues(): issue.project = self._get_project(issue.project) issue.milestone = self._get_milestone(issue.milestone) issue.priority = self._get_priority(issue.priority) issue.assignee = self._get_assignee(issue.assigned_to) issue.status = self._get_issue_status(issue.status) circle_issues.append(issue) return circle_issues def get_issue_by_id(self, issue_id)- 
Get issue
Args
issue_id- the id of the desired issue
 
Returns
Issue object- issue
 
Expand source code
def get_issue_by_id(self, issue_id): """Get issue Args: issue_id: the id of the desired issue Returns: Issue object: issue """ return CircleIssue(self, self.api.issues.get(issue_id)) def get_issue_custom_fields(self, id)- 
Get Issue Custom fields
Args
id:int- Issue id
 
Returns
List- List of dictionaries {name: "custom field name", value: {values as dict}}
 
Expand source code
def get_issue_custom_fields(self, id): """Get Issue Custom fields Args: id (int): Issue id Returns: List: List of dictionaries {name: "custom field name", value: {values as dict}} """ issue = self.api.issues.get(id) issue_attributes = issue.get_attributes()["attributes_values"] project_attributes = self._get_project(issue.project).list_issue_attributes() custom_fields = [] for p_attr in project_attributes: for k, value in issue_attributes.items(): if p_attr.id == int(k): try: custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)}) except: custom_fields.append({"name": p_attr.name, "value": value}) break return custom_fields def get_story_custom_fields(self, id)- 
Get User_Story Custom fields
Args
id:int- User_Story id
 
Returns
List- List of dictionaries {name: "custom field name", value: {values as dict}}
 
Expand source code
def get_story_custom_fields(self, id): """Get User_Story Custom fields Args: id (int): User_Story id Returns: List: List of dictionaries {name: "custom field name", value: {values as dict}} """ user_story = self.api.user_stories.get(id) user_story_attributes = user_story.get_attributes()["attributes_values"] project_attributes = self._get_project(user_story.project).list_user_story_attributes() custom_fields = [] for p_attr in project_attributes: for k, value in user_story_attributes.items(): if p_attr.id == int(k): try: custom_fields.append({"name": p_attr.name, "value": yaml.full_load(value)}) except: custom_fields.append({"name": p_attr.name, "value": value}) break return custom_fields def get_user_circles(self, username)- 
Get circles owned by user
Args
username:str- Name of the user
 
Expand source code
def get_user_circles(self, username): """Get circles owned by user Args: username (str): Name of the user """ user_id = self._get_user_id(username) circles = self.api.projects.list(member=user_id) user_circles = [] for circle in circles: if circle.owner["id"] == user_id: user_circles.append(self._resolve_object(circle)) return user_circles def get_user_stories(self, username)- 
Get all stories of a user
Args
username:str- Name of the user
 
Expand source code
def get_user_stories(self, username): """Get all stories of a user Args: username (str): Name of the user """ user_id = self._get_user_id(username) user_stories = self.api.user_stories.list(assigned_to=user_id) user_stories = [] for user_story in user_stories: # user_story.project = self._get_project(user_story.project) # user_story.milestone = self._get_milestone(user_story.milestone) user_story.status = self._get_user_stories_status(user_story.status) user_stories.append(user_story) return user_stories def get_user_tasks(self, username)- 
Get all tasks of a user
Args
username:str- Name of the user
 
Expand source code
def get_user_tasks(self, username): """Get all tasks of a user Args: username (str): Name of the user """ user_id = self._get_user_id(username) user_tasks = self.api.tasks.list(assigned_to=user_id) user_tasks = [] for user_task in user_tasks: # user_task.project = self._get_project(user_task.project) # user_task.milestone = self._get_milestone(user_task.milestone) user_task.status = self._get_task_status(user_task.status) user_tasks.append(self._resolve_object(user_task)) return user_tasks def import_from_yaml(self, import_dir='/tmp/export_dir')- 
Import Circle with all stories, issues and tasks from yaml files
Args
import_dir:str- import directory path. Defaults to "/tmp/export_dir".
 
Expand source code
def import_from_yaml(self, import_dir="/tmp/export_dir"): """Import Circle with all stories, issues and tasks from yaml files Args: import_dir (str): import directory path. Defaults to "/tmp/export_dir". """ # Helper Functions def check_by_name(list_obj, name_to_find): for obj in list_obj: if obj.name == name_to_find: return obj.id return list_obj[0].id def import_circle(self, yaml_obj): circle = None # Funnel Circle if yaml_obj["basic_info"]["name"].lower() == "funnel": circle = self.create_new_funnel_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Team Circle elif yaml_obj["basic_info"]["name"].lower() == "team": circle = self.create_new_team_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Project Circle elif yaml_obj["basic_info"]["name"].lower() == "project": circle = self.create_new_project_circle( yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"], ) # Any Other Circle else: circle = self._create_new_circle(yaml_obj["basic_info"]["name"], yaml_obj["basic_info"]["description"],) circle.is_backlog_activated = yaml_obj["modules"]["is_backlog_activated"] circle.is_issues_activated = yaml_obj["modules"]["is_issues_activated"] circle.is_kanban_activated = yaml_obj["modules"]["is_kanban_activated"] circle.is_wiki_activated = yaml_obj["modules"]["is_wiki_activated"] circle.is_private = yaml_obj["basic_info"]["is_private"] circle.videoconferences = yaml_obj["modules"]["videoconferences"] for issue_attr in yaml_obj["issues_attributes"]: circle.add_issue_attribute(issue_attr) for us_attr in yaml_obj["stories_attributes"]: circle.add_user_story_attribute(us_attr) return circle def import_story(circle_object, yaml_obj): status_id = check_by_name(circle_object.us_statuses, yaml_obj["status"]["name"]) story = circle_object.add_user_story( yaml_obj["basic_info"].get("subject"), tags=yaml_obj["basic_info"].get("tags"), description=yaml_obj["basic_info"].get("description", ""), client_requirement=yaml_obj["requirements"].get("client_requirement"), team_requirement=yaml_obj["requirements"].get("team_requirement"), is_blocked=yaml_obj["additional_info"].get("is_blocked"), due_date=yaml_obj["date"].get("due_date"), status=status_id, ) for field in yaml_obj.get("custom_fields", []): for attr in circle_object.list_user_story_attributes(): if attr.name == field["name"]: story.set_attribute(attr.id, field["value"]) break return story def import_issue(circle_object, yaml_obj): priority_id = check_by_name(circle_object.priorities, yaml_obj["priority"]["name"]) status_id = check_by_name(circle_object.issue_statuses, yaml_obj["status"]["name"]) type_id = check_by_name(circle_object.issue_types, yaml_obj["type"]["name"]) severity_id = check_by_name(circle_object.severities, yaml_obj["severity"]["name"]) issue = circle_object.add_issue( yaml_obj["basic_info"]["subject"], priority_id, status_id, type_id, severity_id, description=yaml_obj["basic_info"].get("description"), ) for field in yaml_obj["custom_fields"]: for attr in circle_object.list_issue_attributes(): if attr.name == field["name"]: issue.set_attribute(attr.id, field["value"]) break return issue def import_tasks(circle_object, story_object, yaml_obj): status_id = check_by_name(circle_object.task_statuses, yaml_obj["status"]["name"]) task = story_object.add_task( yaml_obj["basic_info"].get("subject"), status_id, description=yaml_obj["basic_info"].get("description", ""), tags=yaml_obj["basic_info"].get("tags"), ) return task # Folders Path projects_path = j.sals.fs.join_paths(import_dir, "projects") stories_path = j.sals.fs.join_paths(import_dir, "stories") issues_path = j.sals.fs.join_paths(import_dir, "issues") tasks_path = j.sals.fs.join_paths(import_dir, "tasks") # List of Files inside project Folder projects = j.sals.fs.os.listdir(projects_path) for project_file in projects: if project_file.endswith(".yaml") or project_file.endswith(".yml"): with open(j.sals.fs.join_paths(projects_path, project_file)) as pf: circle_yaml = yaml.full_load(pf) circle_obj = import_circle(self, circle_yaml) j.logger.info(f"<Circle {circle_obj.id} Created>") for story in circle_yaml["stories"]: with open(j.sals.fs.join_paths(stories_path, f"{story}.yaml")) as sf: story_yaml = yaml.full_load(sf) story_obj = import_story(circle_obj, story_yaml) j.logger.info(f"<Story {story_obj.id} Created in Circle {circle_obj.id}>") for task in story_yaml["tasks"]: with open(j.sals.fs.join_paths(tasks_path, f"{task}.yaml")) as tf: task_yaml = yaml.full_load(tf) task_obj = import_tasks(circle_obj, story_obj, task_yaml) j.logger.info(f"<Task {task_obj.id} Created in Story {story_obj.id}>") task_obj.update() story_obj.update() for issue in circle_yaml["issues"]: with open(j.sals.fs.join_paths(issues_path, f"{issue}.yaml")) as isf: issue_yaml = yaml.full_load(isf) issue_obj = import_issue(circle_obj, issue_yaml) j.logger.info(f"<Issue {issue_obj.id} Created in Circle {circle_obj.id}>") issue_obj.update() circle_obj.update() j.logger.info(f"<Circle {circle_obj.id} Imported with All Stories and Issues") def list_all_active_projects(self, full_info=False)- 
List all projects not starting with "ARHCIVE" HINT: Using full_info will take a longer time
Args
full_info:bool- [description]. Defaults to False.
 
Returns
[type]- [description]
 
Expand source code
def list_all_active_projects(self, full_info=False): """ List all projects not starting with "ARHCIVE" HINT: Using full_info will take a longer time Args: full_info (bool): [description]. Defaults to False. Returns: [type]: [description] """ return [ Circle(self, p) for p in self.list_projects_by(lambda x: not x.name.startswith("ARCHIVE_"), full_info=full_info) ] def list_all_issues(self, username='', full_info=False)- 
List all issues for specific user if you didn't pass user_id will list all the issues HINT: Using full_info will take a longer time
Args
username:str- username.
 full_info:bool- flag used to get object with full info. Defaults to False.
 
Returns
List- List of taiga.models.models.Issue.
 
Expand source code
def list_all_issues(self, username="", full_info=False): """ List all issues for specific user if you didn't pass user_id will list all the issues HINT: Using full_info will take a longer time Args: username (str): username. full_info (bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Issue. """ if username: user_id = self._get_user_id(username) if not full_info: return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list(assigned_to=user_id)] else: return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list(assigned_to=user_id)] else: if not full_info: return [CircleIssue(self, self._resolve_object(x)) for x in self.api.issues.list()] else: return [CircleIssue(self, self.api.issues.get(x.id)) for x in self.api.issues.list()] def list_all_milestones(self)- 
List all milestones
Returns
List- List of taiga.models.models.Milestone.
 
Expand source code
def list_all_milestones(self): """ List all milestones Returns: List: List of taiga.models.models.Milestone. """ return [self._resolve_object(x) for x in self.api.milestones.list()] def list_all_projects(self, full_info=False)- 
List all projects HINT: Using full_info will take a longer time
Args
full_info(bool): flag used to get object with full info. Defaults to False.
Returns
List- List of taiga.models.models.Project.
 
Expand source code
def list_all_projects(self, full_info=False): """ List all projects HINT: Using full_info will take a longer time Args: full_info(bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Project. """ if not full_info: return [Circle(self, self._resolve_object(x)) for x in self.api.projects.list()] else: return [Circle(self, self.api.projects.get(x.id)) for x in self.api.projects.list()] def list_all_tasks(self, username='', full_info=False)- 
List all tasks for specific user if you didn't pass user_id will list all the tasks HINT: Using full_info will take a longer time
Args
username:str- username.
 full_info:bool- flag used to get object with full info. Defaults to False.
 
Returns
List- List of taiga.models.models.Task.
 
Expand source code
def list_all_tasks(self, username="", full_info=False): """ List all tasks for specific user if you didn't pass user_id will list all the tasks HINT: Using full_info will take a longer time Args: username (str): username. full_info (bool): flag used to get object with full info. Defaults to False. Returns: List: List of taiga.models.models.Task. """ if username: user_id = self._get_user_id(username) if not full_info: return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list(assigned_to=user_id)] else: return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list(assigned_to=user_id)] else: if not full_info: return [CircleTask(self, self._resolve_object(x)) for x in self.api.tasks.list()] else: return [CircleTask(self, self.api.tasks.get(x.id)) for x in self.api.tasks.list()] def list_all_user_stories(self, username='', full_info=False)- 
List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time
Args
username:str- username.
 
full_info(bool): flag used to get object with full info. Defaults to False
Returns
List- List of CircleStory.
 
Expand source code
def list_all_user_stories(self, username="", full_info=False): """ List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time Args: username (str): username. full_info(bool): flag used to get object with full info. Defaults to False Returns: List: List of CircleStory. """ if username: user_id = self._get_user_id(username) if not full_info: return [ CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list(assigned_to=user_id) ] else: return [ CircleStory(self, self.api.user_stories.get(x.id)) for x in self.api.user_stories.list(assigned_to=user_id) ] else: if not full_info: return [CircleStory(self, self._resolve_object(x)) for x in self.api.user_stories.list()] else: return [CircleStory(self, self.api.user_stories.get(x.id)) for x in self.api.user_stories.list()] def list_all_users(self, full_info=False)- 
List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time
Args
username:str- username.
 
full_info(bool): flag used to get object with full info. Defaults to False
Returns
List- List of CircleUser.
 
Expand source code
def list_all_users(self, full_info=False): """ List all user stories for specific user if you didn't pass user_id will list all the available user stories HINT: Using full_info will take a longer time Args: username (str): username. full_info(bool): flag used to get object with full info. Defaults to False Returns: List: List of CircleUser. """ circles = self.list_all_projects() users = set() for c in circles: for m in c.members: users.add(m) return [CircleUser(self, self._get_user_by_id(uid)) for uid in users] def list_funnel_circles(self)- 
Expand source code
def list_funnel_circles(self): return [FunnelCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("FUNNEL_"))] def list_project_circles(self)- 
Expand source code
def list_project_circles(self): return [ProjectCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("PROJECT_"))] def list_projects_by(self, fn=<function TaigaClient.<lambda>>, full_info=False)- 
Expand source code
def list_projects_by(self, fn=lambda x: True, full_info=False): return [p for p in self.list_all_projects(full_info=full_info) if fn(p)] def list_team_circles(self)- 
Expand source code
def list_team_circles(self): return [TeamCircle(self, p) for p in self.list_projects_by(lambda x: x.name.startswith("TEAM_"))] def move_story_to_circle(self, story_id, project_id)- 
Moves a story to another circle/project
Args
story_id:int- User story id
 project_id:int- circle/project id
 
Raises
j.exceptions.NotFound- No user story with specified id found
 j.exceptions.NotFound- No project with specified id found
 j.exceptions.Runtime- [description]
 
Returns
int- New id of the migrated user story
 
Expand source code
def move_story_to_circle(self, story_id, project_id): """Moves a story to another circle/project Args: story_id (int): User story id project_id (int): circle/project id Raises: j.exceptions.NotFound: No user story with specified id found j.exceptions.NotFound: No project with specified id found j.exceptions.Runtime: [description] Returns: int: New id of the migrated user story """ def _get_project_status(project_statuses, status): for project_status in project_statuses: if project_status.name == status: return project_status.id try: user_story = self.api.user_stories.get(story_id) except TaigaRestException: raise j.exceptions.NotFound(f"Couldn't find user story with id: {story_id}") project_stories_statuses = self.api.user_story_statuses.list(project=project_id) status = self._get_user_stories_status(user_story.status) story_status_id = _get_project_status(project_stories_statuses, status) try: migrate_story = self.api.user_stories.create( project=project_id, subject=user_story.subject, assigned_to=user_story.assigned_to, milestone=user_story.milestone, status=story_status_id, tags=user_story.tags, ) except TaigaRestException: raise j.exceptions.NotFound(f"No project with id: {project_id} found") try: comments = self.api.history.user_story.get(story_id) comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"])) for comment in comments: migrate_story.add_comment(comment["comment_html"]) project_tasks_statuses = self.api.task_statuses.list(project=project_id) for task in user_story.list_tasks(): status = self._get_task_status(task.status) task_status_id = _get_project_status(project_tasks_statuses, status) migrate_task = migrate_story.add_task( subject=task.subject, status=task_status_id, due_date=task.due_date, milestone=task.milestone, assigned_to=task.assigned_to, tags=task.tags, project=migrate_story.project, user_story=migrate_story.id, ) comments = self.api.history.task.get(migrate_task.id) comments = sorted(comments, key=lambda c: dateutil.parser.isoparse(c["created_at"])) for comment in comments: migrate_task.add_comment(comment["comment_html"]) except Exception as e: self.api.user_stories.delete(migrate_story.id) raise j.exceptions.Runtime(f"Failed to migrate story error was: {str(e)}") self.api.user_stories.delete(story_id) return migrate_story.id def validate_custom_fields(self, attributes)- 
Validate custom fields values to match our requirements
Args
attributes:List- Output from get_issue/story_custom_fields functions
 
Raises
j.exceptions.Validation- Raise validation exception if any input not valid
 
Returns
bool- Return True if no exception raised and print logs
 
Expand source code
def validate_custom_fields(self, attributes): """Validate custom fields values to match our requirements Args: attributes (List): Output from get_issue/story_custom_fields functions Raises: j.exceptions.Validation: Raise validation exception if any input not valid Returns: bool: Return True if no exception raised and print logs """ for attr in attributes: name = attr.get("name") value = attr.get("value") period = value.get("period", "onetime") duration = value.get("duration", 1) amount = value.get("amount", 0) currency = value.get("currency", "eur") start_date = value.get("start_date", f"{dateutil.utils.today().month}:{dateutil.utils.today().year}",) confidence = value.get("confidence", 100) user = value.get("user") part = value.get("part", "0%") type = value.get("type", "revenue") if name not in ["bookings", "commission"]: raise j.exceptions.Validation( f'Name: ({name}) is unknown custom field, please select one of the following ["bookings", "commission"]' ) if period not in ["onetime", "month", "year"]: raise j.exceptions.Validation( f'Period: ({period}) not found, please select one of following ["onetime", "month", "year"]' ) if duration < 1 or duration > 120: raise j.exceptions.Validation(f"Duration: ({duration}) is not in range, please select it from 1 to 120") if not isinstance(amount, int): raise j.exceptions.Validation(f"Amount: ({amount}) is not integer, please add int value") if currency.replace(" ", "").lower() not in [ "usd", "chf", "eur", "gbp", "egp", ]: raise j.exceptions.Validation( f'Currency: ({currency}) is not supported, please use one of the following currencies ["usd", "chf", "eur", "gbp", "egp"]' ) try: date = start_date.split(":") month = int(date[0]) year = int(date[1]) if len(date) > 1 else dateutil.utils.today().year if month < 1 or month > 12: raise j.exceptions.Validation( "Please use values from 1 to 12 in Month field, follow format like MONTH:YEAR as 11:2020 or MONTH as 11" ) except ValueError as e: raise j.exceptions.Validation( "Please use numeric date with the following format MONTH:YEAR as 11:2020 or MONTH as 11" ) except AttributeError as e: pass # Will check what happen if start_date not provide if confidence % 10 != 0: j.exceptions.Validation(f"Confidence: ({confidence}) not multiple of 10, it must be multiple of 10") part_tmp = part.replace("%", "") if user != None and user not in self.list_all_users(): raise j.exceptions.Validation(f"User: ({user}) is not found") if int(part_tmp) < 0 or int(part_tmp) > 100: j.exceptions.Validation(f"Part: ({part}) is a not a valid percentage, it must be from 0% to 100%") if type not in ["revenue", "booking"]: raise j.exceptions.Validation( f'Type: ({type}) is not supported type, please choose one of the following ["revenue" , "booking"]' ) j.logger.info(f"Attribute: {name} passed") return True 
Inherited members