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) """
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'
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
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
class Entity(base.TogglEntity): string = fields.StringField() integer = fields.IntegerField() boolean = fields.BooleanField() float = fields.FloatField()
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'])
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
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
class EvaluateConditionsEntity(base.TogglEntity): string = fields.StringField() integer = fields.IntegerField() boolean = fields.BooleanField() set = fields.SetField()
class Entity(base.TogglEntity): string = fields.StringField() integer = fields.IntegerField() boolean = fields.BooleanField() datetime = fields.DateTimeField()
class MetaTestEntity(metaclass=base.TogglEntityMeta): id = fields.IntegerField() string = fields.StringField() boolean = fields.BooleanField() mapped = fields.MappingField(RandomEntity, 'mapping_field')