예제 #1
0
파일: models.py 프로젝트: mundo03/toggl-cli
class Task(PremiumEntity):
    """
    Task entity.

    This entity is available only for Premium workspaces.
    """

    name = fields.StringField(required=True)
    """
    Name of task
    """

    project = fields.MappingField(Project, 'pid', required=True)
    """
    Project to which the Task is linked to.
    """

    user = fields.MappingField(User, 'uid')
    """
    User to which the Task is assigned to.
    """

    estimated_seconds = fields.IntegerField()
    """
    Estimated duration of task in seconds.
    """

    active = fields.BooleanField(default=True)
    """
    Whether the task is done or not.
    """

    tracked_seconds = fields.IntegerField(write=False)
    """
예제 #2
0
    def test_make_fields(self):
        fields_set = {
            'id': fields.IntegerField(),
            'str': fields.StringField(default='asd'),
            'req_str': fields.StringField(required=True),
            'something_random': 'asdf'
        }

        result = base.TogglEntityMeta._make_fields(fields_set, [RandomEntity])

        # Four because there is one Field taken from RandomEntity and 'something_random' is ignored
        assert len(result) == 4
        assert result['id'].name == 'id'
예제 #3
0
    def test_make_mapped_fields(self):
        mapping_field_instance = fields.MappingField(RandomEntity, 'mapped_field')

        fields_set = {
            'id': fields.IntegerField(),
            'str': fields.StringField(),
            'mapping': mapping_field_instance,
            'something_random': 'asdf'
        }

        result = base.TogglEntityMeta._make_mapped_fields(fields_set)

        # Four because there is one Field taken from RandomEntity and 'something_random' is ignored
        assert len(result) == 1
        assert result['mapped_field'] is mapping_field_instance
예제 #4
0
    def test_make_signature(self):
        fields_set = {
            'id': fields.IntegerField(),
            'str': fields.StringField(default='asd'),
            'req_str': fields.StringField(required=True),
        }

        self.set_fields_names(fields_set)

        sig = base.TogglEntityMeta._make_signature(fields_set)
        sig_params = sig.parameters

        assert len(sig_params) == 2
        assert sig_params['str'].default == 'asd'
        assert 'id' not in sig_params
예제 #5
0
class Entity(base.TogglEntity):
    string = fields.StringField()
    integer = fields.IntegerField()
    boolean = fields.BooleanField()
    float = fields.FloatField()
예제 #6
0
파일: models.py 프로젝트: mundo03/toggl-cli
class Workspace(base.TogglEntity):
    _can_create = False
    _can_delete = False

    name = fields.StringField(required=True)
    """
    Name of the workspace
    """

    premium = fields.BooleanField()
    """
    If it's a pro workspace or not. Shows if someone is paying for the workspace or not
    """

    admin = fields.BooleanField()
    """
    Shows whether currently requesting user has admin access to the workspace
    """

    only_admins_may_create_projects = fields.BooleanField()
    """
    Whether only the admins can create projects or everybody
    """

    only_admins_see_billable_rates = fields.BooleanField()
    """
    Whether only the admins can see billable rates or everybody
    """

    rounding = fields.IntegerField()
    """
    Type of rounding:

    * round down: -1
    * nearest: 0
    * round up: 1
    """

    rounding_minutes = fields.IntegerField()
    """
    Round up to nearest minute
    """

    default_hourly_rate = fields.FloatField()
    """
    Default hourly rate for workspace, won't be shown to non-admins
    if the only_admins_see_billable_rates flag is set to true
    """

    default_currency = fields.StringField()
    """
    Default currency for workspace
    """

    # As TogglEntityMeta is by default adding WorkspaceTogglSet to TogglEntity,
    # but we want vanilla TogglSet so defining it here explicitly.
    objects = base.TogglSet()

    def invite(self, *emails):  # type: (str) -> None
        """
        Invites users defined by email addresses. The users does not have to have account in Toggl, in that case after
        accepting the invitation, they will go through process of creating the account in the Toggl web.

        :param emails: List of emails to invite.
        :return: None
        """
        for email in emails:
            if not validate_email(email):
                raise exceptions.TogglValidationException(
                    'Supplied email \'{}\' is not valid email!'.format(email))

        emails_json = json.dumps({'emails': emails})
        data = utils.toggl("/workspaces/{}/invite".format(self.id),
                           "post",
                           emails_json,
                           config=self._config)

        if 'notifications' in data and data['notifications']:
            raise exceptions.TogglException(data['notifications'])
예제 #7
0
파일: models.py 프로젝트: mundo03/toggl-cli
class Project(WorkspacedEntity):
    """
    Project entity
    """

    name = fields.StringField(required=True)
    """
    Name of the project. (Required)
    """

    client = fields.MappingField(Client, 'cid')
    """
    Client associated to the project.
    """

    active = fields.BooleanField(default=True)
    """
    Whether the project is archived or not. (Default: True)
    """

    is_private = fields.BooleanField(default=True)
    """
    Whether project is accessible for only project users or for all workspace users. (Default: True)
    """

    billable = fields.BooleanField(premium=True)
    """
    Whether the project is billable or not.

    (Available only for Premium workspaces)
    """

    auto_estimates = fields.BooleanField(default=False, premium=True)
    """
    Whether the estimated hours are automatically calculated based on task estimations or manually
    fixed based on the value of 'estimated_hours'.

    (Available only for Premium workspaces)
    """

    estimated_hours = fields.IntegerField(premium=True)
    """
    If auto_estimates is true then the sum of task estimations is returned, otherwise user inserted hours.

    (Available only for Premium workspaces)
    """

    color = fields.IntegerField()
    """
    Id of the color selected for the project
    """

    hex_color = fields.StringField()
    """
    Hex code of the color selected for the project
    """

    rate = fields.FloatField(premium=True)
    """
    Hourly rate of the project.

    (Available only for Premium workspaces)
    """
    def add_user(
        self,
        user,
        manager=False,
        rate=None
    ):  # type: (User, bool, typing.Optional[float]) -> ProjectUser
        """
        Add new user to a project.

        :param user: User to be added
        :param manager: Specifies if the user should have manager's rights
        :param rate: Rate for billing
        :return: ProjectUser instance.
        """
        project_user = ProjectUser(project=self,
                                   user=user,
                                   workspace=self.workspace,
                                   manager=manager,
                                   rate=rate)
        project_user.save()

        return project_user
예제 #8
0
파일: base.py 프로젝트: tbrodbeck/toggl-cli
class TogglEntity(metaclass=TogglEntityMeta):
    """
    Base class for all Toggl Entities.

    Simplest Entities consists only of fields declaration (eq. TogglField and its subclasses), but it is also possible
    to implement custom class or instance methods for specific tasks.

    This class handles serialization, saving new instances, updating the existing one, deletion etc.
    Support for these operation can be customized using _can_* attributes, by default everything is enabled.
    """

    __signature__ = Signature()
    __fields__ = OrderedDict()

    _validate_workspace = True
    _can_create = True
    _can_update = True
    _can_delete = True
    _can_get_detail = True
    _can_get_list = True

    id = model_fields.IntegerField(required=False, default=None)

    objects = None  # type: TogglSet

    def __init__(self, config=None, **kwargs):
        self._config = config or utils.Config.factory()
        self.__change_dict__ = {}

        for field in self.__fields__.values():
            if field.name in {'id'}:
                continue

            if isinstance(field, model_fields.MappingField):
                # User supplied most probably the whole mapped object
                if field.name in kwargs:
                    field.init(self, kwargs.get(field.name))
                    continue

                # Most probably converting API call with direct ID of the object
                if field.mapped_field in kwargs:
                    field.init(self, kwargs.get(field.mapped_field))
                    continue

                if field.default is model_fields.NOTSET and field.required:
                    raise TypeError('We need \'{}\' attribute!'.format(field.mapped_field))
                continue

            if field.name not in kwargs:
                if field.default is model_fields.NOTSET and field.required:
                    raise TypeError('We need \'{}\' attribute!'.format(field.name))
            else:  # Set the attribute only when there is some value to set, so default values could work properly
                field.init(self, kwargs[field.name])

    def save(self, config=None):  # type: (utils.Config) -> None
        """
        Main method for saving the entity.

        If it is a new entity (eq. entity.id is not set), then calling this method will result in creation of new object using POST call.
        If this is already existing entity, then calling this method will result in updating of the object using PUT call.

        For updating the entity, only changed fields are sent (this is tracked using self.__change_dict__).

        Before the API call validations are performed on the instance and only after successful validation, the call is made.

        :raises exceptions.TogglNotAllowedException: When action (create/update) is not allowed.
        """
        if not self._can_update and self.id is not None:
            raise exceptions.TogglNotAllowedException('Updating this entity is not allowed!')

        if not self._can_create and self.id is None:
            raise exceptions.TogglNotAllowedException('Creating this entity is not allowed!')

        config = config or self._config

        self.validate()

        if self.id is not None:  # Update
            utils.toggl('/{}/{}'.format(self.get_url(), self.id), 'put', self.json(update=True), config=config)
            self.__change_dict__ = {}  # Reset tracking changes
        else:  # Create
            data = utils.toggl('/{}'.format(self.get_url()), 'post', self.json(), config=config)
            self.id = data['data']['id']  # Store the returned ID

    def delete(self, config=None):  # type: (utils.Config) -> None
        """
        Method for deletion of the entity through API using DELETE call.

        This will not delete the instance's object in Python, therefore calling save() method after deletion will
        result in new object created using POST call.

        :raises exceptions.TogglNotAllowedException: When action is not allowed.
        """
        if not self._can_delete:
            raise exceptions.TogglNotAllowedException('Deleting this entity is not allowed!')

        if not self.id:
            raise exceptions.TogglException('This instance has not been saved yet!')

        utils.toggl('/{}/{}'.format(self.get_url(), self.id), 'delete', config=config or self._config)
        self.id = None  # Invalidate the object, so when save() is called after delete a new object is created

    def json(self, update=False):  # type: (bool) -> str
        """
        Serialize the entity into JSON string.

        :param update: Specifies if the resulted JSON should contain only changed fields (for PUT call) or whole entity.
        """
        return json.dumps({self.get_name(): self.to_dict(serialized=True, changes_only=update)})

    def validate(self):  # type: () -> None
        """
        Performs validation across all Entity's fields.

        If overloading then don't forget to call super().validate()!
        """
        for field in self.__fields__.values():
            try:
                value = field._get_value(self)
            except AttributeError:
                value = None

            field.validate(value, self)

    def to_dict(self, serialized=False, changes_only=False):  # type: (bool, bool) -> typing.Dict
        """
        Method that returns dict representing the instance.

        :param serialized: If True, the returned dict contains only Python primitive types and no objects (eq. so JSON serialization could happen)
        :param changes_only: If True, the returned dict contains only changes to the instance since last call of save() method.
        """
        source_dict = self.__change_dict__ if changes_only else self.__fields__
        entity_dict = {}
        for field_name in source_dict.keys():
            try:
                field = self.__fields__[field_name]
            except KeyError:
                field = self.__mapped_fields__[field_name]

            try:
                value = field._get_value(self)
            except AttributeError:
                value = None

            if serialized:
                try:
                    entity_dict[field.mapped_field] = field.serialize(value)
                except AttributeError:
                    entity_dict[field.name] = field.serialize(value)
            else:
                entity_dict[field.name] = value

        return entity_dict

    def __eq__(self, other):  # type: (typing.Generic[Entity]) -> bool
        if not isinstance(other, self.__class__):
            return False

        if self.id is None or other.id is None:
            raise RuntimeError('One of the instances was not yet saved! We can\'t compere unsaved instances!')

        return self.id == other.id

    # TODO: [Q/Design] Problem with unique field's. Copy ==> making invalid option ==> Some validation?
    def __copy__(self):  # type: () -> typing.Generic[Entity]
        cls = self.__class__
        new_instance = cls.__new__(cls)
        new_instance.__dict__.update(self.__dict__)
        new_instance.id = None  # New instance was never saved ==> no ID for it yet
        return new_instance

    def __str__(self):  # type: () -> str
        return '{} (#{})'.format(getattr(self, 'name', None) or self.__class__.__name__, self.id)

    @classmethod
    def get_name(cls, verbose=False):  # type: (bool) -> str
        name = cls.__name__
        name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()

        if verbose:
            return name.replace('_', ' ').capitalize()

        return name

    @classmethod
    def get_url(cls):  # type: () -> str
        return cls.get_name() + 's'

    @classmethod
    def deserialize(cls, config=None, **kwargs):  # type: (utils.Config, **typing.Any) -> typing.Generic[Entity]
        """
        Method which takes kwargs as dict representing the Entity's data and return actuall instance of the Entity.
        """
        try:
            kwargs.pop('at')
        except KeyError:
            pass

        instance = cls.__new__(cls)
        instance._config = config
        instance.__change_dict__ = {}

        for key, field in instance.__fields__.items():
            try:
                value = kwargs[key]
            except KeyError:
                try:
                    value = kwargs[field.mapped_field]
                except (KeyError, AttributeError):
                    continue

            field.init(instance, value)

        return instance
예제 #9
0
class EvaluateConditionsEntity(base.TogglEntity):
    string = fields.StringField()
    integer = fields.IntegerField()
    boolean = fields.BooleanField()
    set = fields.SetField()
예제 #10
0
class Entity(base.TogglEntity):
    string = fields.StringField()
    integer = fields.IntegerField()
    boolean = fields.BooleanField()
    datetime = fields.DateTimeField()
예제 #11
0
class MetaTestEntity(metaclass=base.TogglEntityMeta):
    id = fields.IntegerField()
    string = fields.StringField()
    boolean = fields.BooleanField()
    mapped = fields.MappingField(RandomEntity, 'mapping_field')