Skip to content

Card

CLASS DESCRIPTION
Card

Python interface for Planka Cards

Stopwatch

Python interface for Planka Stopwatches

Card

Card(schema: Schema, session: Planka)

Bases: PlankaModel[Card]

Python interface for Planka Cards

METHOD DESCRIPTION
add_attachment

Add an Attachment to the card

add_card_fields

Add fields directly to a Card

add_label

Add a Label to the Card

add_labels

Add multiple Labels to the Card

add_location

Add a point location to a card as a FieldGroup

add_member

Add a User to the Card

add_members

Add multiple members to a Card

add_task_list

Add a TaskList to the Card

comment

Leave a comment as this user and mention any user included in the mentions list

copy

Create a deepcopy of the model and its associated schema.

create_card_field_group

Create a CustomFieldGroup in the Card

create_task_list

Create a NEW TaskList to the Card

delete

Delete the Card

diff

Get a schema diff between two model schemas.

duplicate

Duplicate the card in the current List

get_field_values

Get a mapping of CustomFields to CustomFieldValues

move

Move the card to a new list (default to top of new list)

read_notifications

Read all the current User's Notifications for the Card

remove_label

Remove the Label from the Card

remove_labels

Remove the Label from the Card

remove_member

Remove a User member from the Card

remove_members

Remove multiple members from a Card

restore

Restore the Card from arcive/trash to its previous list

sync

Sync the Card with the Planka server

update

Update the Card

ATTRIBUTE DESCRIPTION
__formatter__

Formatter func that allows overriding str behavior for models

TYPE: ModelFormatter[Self]

actions

Get all Actions associated with the Card

TYPE: list[Action]

attachments

Get all Attachments associated with the Card

TYPE: list[Attachment]

board

The Board the Card belongs to

TYPE: Board

card_labels

Get all CardLabel associations for the Card

TYPE: list[CardLabel]

card_memberships

Get all CardMemberships associated with the Card

TYPE: list[CardMembership]

comments

Get all Comments on the Card

TYPE: list[Comment]

comments_count

Total number of comments on the Card

TYPE: int

cover

The Attachment used as cover (None if no cover)

TYPE: Attachment | None

created_at

When the Card was created

TYPE: datetime

creator

The User who Created the card

TYPE: User | None

custom_field_groups

Get all CustomFieldGroups associated with the Card

TYPE: list[CustomFieldGroup]

custom_field_values

Get all CustomFieldValues associated with the Card

TYPE: list[CustomFieldValue]

custom_fields

Get all CustomFields associated with the Card

TYPE: list[CustomField]

description

Detailed description of the Card

TYPE: str

due_date

Due date for the card

TYPE: datetime | None

due_date_completed

Whether the due date is completed

TYPE: bool

formal_name

Get a formal name for the card {Project}->{Board}->{List}->{Card}

TYPE: str

is_closed

Whether the Card is closed

TYPE: bool

labels

Get all Labels associated with the Card

TYPE: list[Label]

list

The List the Card belongs to

TYPE: List

list_changed_at

When the Card was last moved between Lists

TYPE: datetime

members

Get all Users Assigned to the card

TYPE: list[User]

name

Name/title of the Card

TYPE: str

position

Position of the Card within the List

TYPE: int

prev_list

The previous List the card was in (available when in archive or trash)

TYPE: List | None

project

Get the Project that the card is in

TYPE: Project

stopwatch

Stopwatch for time tracking

TYPE: Stopwatch

subscribed

If the current user is subscribed to the Card

TYPE: bool

task_lists

Get all TaskLists associated with the Card

TYPE: list[TaskList]

tasks

Get all Tasks associated with the card

TYPE: list[Task]

type

Type of the Card

updated_at

When the Card was last updated

TYPE: datetime

url

The URL to the card

TYPE: str

users

Get all Users associated with the Card (including Creator)

TYPE: list[User]

Source code in src/plankapy/v2/models/_base.py
30
31
32
33
34
35
36
def __init__(self, schema: Schema, session: Planka) -> None:
    self._schema = schema
    self.session = session
    self.endpoints = session.endpoints
    self.client = session.client
    self.current_role = session.current_role
    self.current_id = session.current_id

__formatter__ class-attribute instance-attribute

__formatter__: ModelFormatter[Self] = DEFAULT_FORMATTER

Formatter func that allows overriding str behavior for models

actions property

actions: list[Action]

Get all Actions associated with the Card

attachments property

attachments: list[Attachment]

Get all Attachments associated with the Card

board property

board: Board

The Board the Card belongs to

card_labels property

card_labels: list[CardLabel]

Get all CardLabel associations for the Card

card_memberships property

card_memberships: list[CardMembership]

Get all CardMemberships associated with the Card

comments property

comments: list[Comment]

Get all Comments on the Card

comments_count property

comments_count: int

Total number of comments on the Card

cover property writable

cover: Attachment | None

The Attachment used as cover (None if no cover)

created_at property

created_at: datetime

When the Card was created

creator property

creator: User | None

The User who Created the card

Note

If the creator is no longer on the Board, only Admins and Project Owners can see them. Otherwise, None will be returned

If the User has been deleted, Admins and Project Owners will get a Server error

custom_field_groups property

custom_field_groups: list[CustomFieldGroup]

Get all CustomFieldGroups associated with the Card

custom_field_values property

custom_field_values: list[CustomFieldValue]

Get all CustomFieldValues associated with the Card

custom_fields property

custom_fields: list[CustomField]

Get all CustomFields associated with the Card

description property writable

description: str

Detailed description of the Card

due_date property writable

due_date: datetime | None

Due date for the card

due_date_completed property writable

due_date_completed: bool

Whether the due date is completed

formal_name property

formal_name: str

Get a formal name for the card {Project}->{Board}->{List}->{Card}

is_closed property

is_closed: bool

Whether the Card is closed

labels property

labels: list[Label]

Get all Labels associated with the Card

list_changed_at property

list_changed_at: datetime

When the Card was last moved between Lists

members property

members: list[User]

Get all Users Assigned to the card

name property writable

name: str

Name/title of the Card

position property writable

position: int

Position of the Card within the List

prev_list property

prev_list: List | None

The previous List the card was in (available when in archive or trash)

project property

project: Project

Get the Project that the card is in

stopwatch property

stopwatch: Stopwatch

Stopwatch for time tracking

subscribed property writable

subscribed: bool

If the current user is subscribed to the Card

task_lists property

task_lists: list[TaskList]

Get all TaskLists associated with the Card

tasks property

tasks: list[Task]

Get all Tasks associated with the card

type property writable

type

Type of the Card

updated_at property

updated_at: datetime

When the Card was last updated

url property

url: str

The URL to the card

users property

users: list[User]

Get all Users associated with the Card (including Creator)

add_attachment

add_attachment(attachment: str | bytes, *, cover: bool = False, download_url: bool = False, name: str | None = None) -> Attachment

Add an Attachment to the card

PARAMETER DESCRIPTION

attachment

The URL or raw bytes of the attachment

TYPE: str | bytes

cover

Set the new attachment as the cover of the card

TYPE: bool DEFAULT: False

download_url

If a link is used, download the file from the link and attach it (default: False)

TYPE: bool DEFAULT: False

name

The optional name of the attachment (default is hash() + mimetypes.guess_type(attachment))

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Attachment

Attachment

RAISES DESCRIPTION
HTTPStatusError

If the url cannot be downloaded

OSError

If a local file cannot be opened and read

Source code in src/plankapy/v2/models/card.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
def add_attachment(self, attachment: str | bytes, 
                   *, 
                   cover: bool=False, 
                   download_url: bool=False,
                   name: str | None=None) -> Attachment:
    """Add an Attachment to the card

    Args:
        attachment (str | bytes): The URL or raw bytes of the attachment
        cover (bool): Set the new attachment as the cover of the card
        download_url (bool): If a link is used, download the file from the link and attach it (default: `False`)
        name (str | None): The optional name of the attachment (default is `hash() + mimetypes.guess_type(attachment)`)

    Returns:
        Attachment

    Raises:
        HTTPStatusError: If the url cannot be downloaded
        OSError: If a local file cannot be opened and read
    """
    # Force a PermissionError early if the user isn't a board editor
    if self.session.current_id not in [e.id for e in self.board.editors]:
        self.endpoints.createAttachment(self.id, type='link', url='nourl', name='NO_PERMISSION')

    # Deferred import of mimetypes that is only used here
    # This function takes so long anyways so the import delay 
    # isn't noticable
    import mimetypes

    # Handle filepath or URL
    mime_type = None
    extension = '.bin'
    if isinstance(attachment, str):

        # Guess URL file type
        if attachment.startswith('http'):
            mime_type, *_ = mimetypes.guess_type(attachment)
            mime_type = mime_type or 'application/octet-stream'
            extension = mimetypes.guess_extension(mime_type) or '.bin'

            # Download the file if requested
            if download_url:
                try:
                    req = self.client.get(attachment)
                    attachment = req.raise_for_status().read()
                except HTTPStatusError as status_error:
                    status_error.add_note(f'Unable to download attachment from {attachment}')
                    raise

            # Attach a link otherwise
            else:
                return Attachment(
                    self.endpoints.createAttachment(
                        self.id, 
                        type='link', 
                        url=attachment, 
                        name=name or f'{name or hash(attachment)}{extension}')['item'], 
                    self.session
                )

        # Guess local file type
        # And read Bytes
        else:
            mime_type, *_ = mimetypes.guess_file_type(attachment)
            mime_type = mime_type or 'application/octet-stream'
            attachment = open(attachment, 'rb').read()

    mime_type = mime_type or 'application/octet-stream'
    extension = mimetypes.guess_extension(mime_type) or '.bin'
    name = f'{name or hash(attachment)}'
    if not name.endswith(extension):
        name = f'{name}{extension}'        
    a = Attachment(
        self.endpoints.createAttachment(
            self.id, 
            name=name,
            type='file',
            file=bytes(attachment),
            requestId=str(abs(hash(datetime.now().isoformat()))),
            mime_type=mime_type,
        )['item'], 
        self.session
    )
    if cover:
        self.cover = a
    return a

add_card_fields

add_card_fields(*fields: str, group: str = 'Fields', position: Position = 'top') -> CustomFieldGroup

Add fields directly to a Card

PARAMETER DESCRIPTION

*fields

Varargs of the Fieldnames to add to the Card

TYPE: str DEFAULT: ()

group

An optional FieldGroup name to add the fields to (default: Fields)

TYPE: str DEFAULT: 'Fields'

position

The position to add the Card field group at (default: top)

TYPE: Position DEFAULT: 'top'

RETURNS DESCRIPTION
CustomFieldGroup

CustomFieldGroup

Source code in src/plankapy/v2/models/card.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def add_card_fields(self, *fields: str,
                    group: str='Fields', 
                    position: Position='top') -> CustomFieldGroup:
    """Add fields directly to a Card

    Args:
        *fields: Varargs of the Fieldnames to add to the Card
        group: An optional FieldGroup name to add the fields to (default: `Fields`)
        position: The position to add the Card field group at (default: `top`)

    Returns:
        CustomFieldGroup
    """
    # Get an existing group if name matches
    cfg = (
        self.custom_field_groups[{'name': group}].dpop() 
        or self.create_card_field_group(group, position)
    )
    # Add any fields that don't already exist in the Group
    cfg.add_fields(*(set(fields) ^ set(cfg.custom_fields.extract('name'))))
    return cfg

add_label

add_label(label: Label, *, add_to_board: bool = False) -> CardLabel

Add a Label to the Card

PARAMETER DESCRIPTION

label

The Label to add to the card

TYPE: Label

add_to_board

If the Label is not in the board, add it (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
CardLabel

The CardLabel relationship

TYPE: CardLabel

Note

When using add_to_board The label position will default to the top of the label list

Source code in src/plankapy/v2/models/card.py
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def add_label(self, label: Label, 
              *, 
              add_to_board: bool=False) -> CardLabel:
    """Add a Label to the Card

    Args:
        label (Label): The Label to add to the card
        add_to_board (bool): If the Label is not in the board, add it (default: `False`)

    Returns:
        CardLabel: The CardLabel relationship

    Note:
        When using `add_to_board` The label position will default to the top of the label list
    """
    # Check if label is already on card
    for card_label in self.card_labels:
        if label.id == card_label.schema['labelId']:
            return card_label

    # Handle adding Label if it doesn't exist
    if label not in self.board.labels and add_to_board:
        label = label.add_to_board(self.board)

    # Create new CardLabel relationship
    return CardLabel(self.endpoints.createCardLabel(self.id, labelId=label.id)['item'], self.session)

add_labels

add_labels(labels: Sequence[Label], *, add_to_board: bool = False) -> list[CardLabel]

Add multiple Labels to the Card

PARAMETER DESCRIPTION

labels

The Labels to add (must be associated with the card.board)

TYPE: Sequence[Label]

add_to_board

If a Label is not in the board, add it (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[CardLabel]

list[CardLabel]: The added CardLabel relations

Note

Any labels that are not on the Card's Board will be skipped

Source code in src/plankapy/v2/models/card.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
@model_list
def add_labels(self, labels: Sequence[Label], 
               *,
               add_to_board: bool=False) -> list[CardLabel]:
    """Add multiple Labels to the Card

    Args:
        labels (Sequence[Label]): The Labels to add (must be associated with the card.board)
        add_to_board (bool): If a Label is not in the board, add it (default: `False`)

    Returns:
        list[CardLabel]: The added CardLabel relations

    Note:
        Any labels that are not on the Card's Board will be skipped
    """
    return [
        self.add_label(
            label, 
            add_to_board=add_to_board
        )
        for label in labels
    ]

add_location

add_location(lat: float, lon: float, name: str, position: Position | int) -> CustomFieldGroup

Add a point location to a card as a FieldGroup

PARAMETER DESCRIPTION

lat

Latitude of the location

TYPE: float

lon

Longitude of the location

TYPE: float

name

Name of the location (FieldGroup name)

TYPE: str

position

Position of the FieldGroup within the Card (default: top)

TYPE: Position | int

Source code in src/plankapy/v2/models/card.py
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
def add_location(self, lat: float, lon: float, name: str, position: Position | int) -> CustomFieldGroup:
    """Add a point location to a card as a FieldGroup

    Args:
        lat (float): Latitude of the location
        lon (float): Longitude of the location
        name (str): Name of the location (FieldGroup name)
        position (Position | int): Position of the FieldGroup within the Card (default: `top`)
    """
    loc_group = self.add_card_fields('latitude', 'longitude', group=name, position=position)
    for field in loc_group.custom_field_values:
        if field.custom_field.name == 'latitude':
            field.content = str(lat)
        elif field.custom_field.name == 'longitude':
            field.content = str(lon)
    return loc_group

add_member

add_member(user: User, *, add_to_board: bool = False, role: BoardRole = 'viewer', can_comment: bool = False) -> CardMembership

Add a User to the Card

PARAMETER DESCRIPTION

user

The User to add to the Card

TYPE: User

add_to_board

Add the User to the Board if they are not already a member

TYPE: bool DEFAULT: False

role

If User is added to board, set role (default: viewer)

TYPE: BoardRole DEFAULT: 'viewer'

can_comment

If User is added as a viewer, set commenting status (default: False)

TYPE: bool DEFAULT: False

Note

Default options for adding to Board abide by least privilege so role and comment must be set

Source code in src/plankapy/v2/models/card.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
def add_member(self, user: User, 
               *, 
               add_to_board: bool=False, 
               role: BoardRole='viewer', 
               can_comment: bool=False) -> CardMembership:
    """Add a User to the Card

    Args:
        user (User): The User to add to the Card
        add_to_board (bool): Add the User to the Board if they are not already a member
        role (BoardRole): If User is added to board, set role (default: `viewer`)
        can_comment (bool): If User is added as a `viewer`, set commenting status (default: `False`)

    Note:
        Default options for adding to Board abide by least privilege so role and comment must be set 
    """
    # User is already a member
    for membership in self.card_memberships:
        if membership.user == user:
            return membership

    # Add the user to the board
    if user not in self.board.users and add_to_board:
        self.board.add_member(user, role=role, can_comment=can_comment if role == 'viewer' else True)

    return CardMembership(self.endpoints.createCardMembership(self.id, userId=user.id)['item'], self.session)

add_members

add_members(users: Sequence[User], *, add_to_board: bool = False, role: BoardRole = 'viewer', can_comment: bool = False) -> list[CardMembership]

Add multiple members to a Card

PARAMETER DESCRIPTION

users

The Users to add to the Card

TYPE: Sequence[User]

add_to_board

Add the User to the Board if they are not already a member

TYPE: bool DEFAULT: False

role

If User is added to board, set role (default: viewer)

TYPE: Literal['viewer', 'editor'] DEFAULT: 'viewer'

can_comment

If User is added as a viewer, set commenting status (default: False)

TYPE: bool DEFAULT: False

Note

Default options for adding to Board abide by least privilege so role and comment must be set

Source code in src/plankapy/v2/models/card.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
@model_list
def add_members(self, users: Sequence[User], 
                *, 
                add_to_board: bool=False, 
                role: BoardRole='viewer', 
                can_comment: bool=False) -> list[CardMembership]:
    """Add multiple members to a Card

    Args:
        users (Sequence[User]): The Users to add to the Card
        add_to_board (bool): Add the User to the Board if they are not already a member
        role (Literal['viewer', 'editor']): If User is added to board, set role (default: `viewer`)
        can_comment (bool): If User is added as a `viewer`, set commenting status (default: `False`)

    Note:
        Default options for adding to Board abide by least privilege so role and comment must be set

    """
    return [
        self.add_member(
            user, 
            add_to_board=add_to_board, 
            role=role, 
            can_comment=can_comment,
        )
        for user in users
    ]

add_task_list

add_task_list(task_list: TaskList, *, name: str | None = None, position: Position | int = 'top', show_on_card: bool | None = None, hide_completed: bool | None = None) -> TaskList

Add a TaskList to the Card

PARAMETER DESCRIPTION

task_list

The TaskList to add

TYPE: TaskList

name

Name override, None will use input name (default: None)

TYPE: str | None DEFAULT: None

postion

The position of the TaskList in the Card (default: top)

TYPE: Position | int

show_on_card

bool | None): Show On Card override, None will use input show (default: None)

TYPE: bool | None DEFAULT: None

hide_completed

bool | None): Hide Completed override , None will use input hide (default: None)

TYPE: bool | None DEFAULT: None

RETURNS DESCRIPTION
TaskList

The TaskList

Source code in src/plankapy/v2/models/card.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
def add_task_list(self, task_list: TaskList, 
                  *, 
                  name: str | None=None,
                  position: Position | int='top',
                  show_on_card: bool | None=None,
                  hide_completed: bool | None=None) -> TaskList:
    """Add a TaskList to the Card

    Args:
        task_list (TaskList): The TaskList to add
        name (str | None): Name override, None will use input name (default: `None`)
        postion (Position | int): The position of the TaskList in the Card (default: `top`)
        show_on_card: bool | None): Show On Card override, None will use input show (default: `None`)
        hide_completed: bool | None): Hide Completed override , None will use input hide (default: `None`)

    Returns:
        The TaskList 
    """
    return self.create_task_list(
        name=name or task_list.name,
        position=get_position(self.task_lists, position),
        show_on_card=show_on_card or task_list.show_on_front_of_card,
        hide_completed=hide_completed or task_list.hide_completed_tasks, 
    )

comment

comment(comment: str, *, mentions: Sequence[User] | None = None) -> Comment

Leave a comment as this user and mention any user included in the mentions list

PARAMETER DESCRIPTION

text

The text body of the comment (@[name|username|email] will mention)

mentions

A sequence of Users that will be mentioned after the body

TYPE: Sequence[User] | None DEFAULT: None

Note

If a user is explicitly mentioned in the comment, they will be removed from the suffix mention. e.g.

>>> card.comment(
...     'Fix this @user1, then send to @user2', 
...     mentions=[user1, user2, user3]
... )
'''Fix this @user1, then send to @user2
@user3'''

Example
    >>> card.comment('Need Fix', mentions=card2.users)
    # Comment from current user On Card:
    Need Fix
    @user1
    @user2
Source code in src/plankapy/v2/models/card.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def comment(self, comment: str, *, mentions: Sequence[User]|None=None) -> Comment:
    """Leave a comment as this user and mention any user included in the mentions list

    Args:
        text: The text body of the comment (`@[name|username|email]` will mention)
        mentions: A sequence of Users that will be mentioned after the body

    Note:
        If a user is explicitly mentioned in the comment, they will be removed from the 
        suffix mention. e.g. 
        ```python
        >>> card.comment(
        ...     'Fix this @user1, then send to @user2', 
        ...     mentions=[user1, user2, user3]
        ... )
        '''Fix this @user1, then send to @user2
        @user3'''
        ```

    Example:
        ```python
            >>> card.comment('Need Fix', mentions=card2.users)
            # Comment from current user On Card:
            Need Fix
            @user1
            @user2
        ```
    """
    # Store inline mentions to prevent additional mention in postfix
    _mentioned: list[User] = []
    if '@' in comment:
        for u in self.board.users:
            # Replace raw @ mentions with markdown formatted mentions
            # Allow mentioning by name, username, or email
            if f'@{u.email}' in comment:
                comment = comment.replace(f'@{u.email}', f'@[{u.email}]({u.id})')
                _mentioned.append(u)
            elif f'@{u.name}' in comment:
                comment = comment.replace(f'@{u.name}', f'@[{u.name}]({u.id})')
                _mentioned.append(u)
            elif f'@{u.username}' in comment:
                comment = comment.replace(f'@{u.username}', f'@[{u.username}]({u.id})')
                _mentioned.append(u)

    # Add additional postfix mentions
    if mentions:
        mentions = [m for m in mentions if m not in _mentioned]
        comment = '\n'.join([comment, *[f"@[{u.name}]({u.id})" for u in mentions or []]])
    return Comment(self.endpoints.createComment(self.id, text=comment)['item'], self.session)

copy

copy() -> Self

Create a deepcopy of the model and its associated schema.

Note

Since the endpoints for both instances of the Model are the same, any calls to update will restore the state and bring both copies into sync. copies like this are meant more for comparing changes when running a sync or update/assignemnt operation.

Example:

    >>> card_copy = card.copy()
    >>> card.name = 'Updated Name'
    >>> card_copy.name
    'Original Name'
    >>> card.name
    'Updated Name'
    >>> # This update may have had side effects
    >>> print(card_copy.diff(card))
    {'name': ('Original Name', 'Updated Name'), 'updatedAt': ('...2:00pm', '...2:45pm'), ...}

Source code in src/plankapy/v2/models/_base.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def copy(self) -> Self:
    """Create a deepcopy of the model and its associated schema.

    Note:
        Since the endpoints for both instances of the Model are the same, any 
        calls to update will restore the state and bring both copies into sync. 
        copies like this are meant more for comparing changes when running a sync 
        or update/assignemnt operation.

    Example:
    ```python
        >>> card_copy = card.copy()
        >>> card.name = 'Updated Name'
        >>> card_copy.name
        'Original Name'
        >>> card.name
        'Updated Name'
        >>> # This update may have had side effects
        >>> print(card_copy.diff(card))
        {'name': ('Original Name', 'Updated Name'), 'updatedAt': ('...2:00pm', '...2:45pm'), ...}
    ```
    """
    return copy.deepcopy(self)

create_card_field_group

create_card_field_group(name: str, position: Position = 'top') -> CustomFieldGroup

Create a CustomFieldGroup in the Card

PARAMETER DESCRIPTION

name

The name of the group

TYPE: str

position

The position of the new group

TYPE: Position DEFAULT: 'top'

Source code in src/plankapy/v2/models/card.py
482
483
484
485
486
487
488
489
490
491
492
493
494
495
def create_card_field_group(self, name: str, position: Position='top') -> CustomFieldGroup:
    """Create a CustomFieldGroup in the Card

    Args:
        name: The name of the group
        position: The position of the new group
    """
    return CustomFieldGroup(
            self.endpoints.createCardCustomFieldGroup(
                self.id, 
                name=name, 
                position=get_position(self.custom_field_groups, position))['item'], 
            self.session
        )

create_task_list

create_task_list(*, name: str, position: Position | int = 'top', show_on_card: bool = False, hide_completed: bool = False) -> TaskList

Create a NEW TaskList to the Card

PARAMETER DESCRIPTION

name str

Name for the TaskList

postion

The position of the TaskList in the Card (default: top)

TYPE: Position | int

show_on_card

bool: Show TaskList on the front of the card (default: False)

TYPE: bool DEFAULT: False

hide_completed

bool: Hide completed tasks (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
TaskList

The TaskList

Source code in src/plankapy/v2/models/card.py
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
def create_task_list(self, 
                  *, 
                  name: str,
                  position: Position | int='top',
                  show_on_card: bool=False,
                  hide_completed: bool=False) -> TaskList:
    """Create a NEW TaskList to the Card

    Args:
        name str: Name for the TaskList
        postion (Position | int): The position of the TaskList in the Card (default: `top`)
        show_on_card: bool: Show TaskList on the front of the card (default: `False`)
        hide_completed: bool: Hide completed tasks (default: `False`)

    Returns:
        The TaskList 
    """
    return TaskList(
        self.endpoints.createTaskList(
            self.id, 
            position=get_position(self.task_lists, position), 
            name=name, 
            showOnFrontOfCard=show_on_card, 
            hideCompletedTasks=hide_completed,
        )['item'], 
        self.session
    )

delete

delete()

Delete the Card

Source code in src/plankapy/v2/models/card.py
286
287
288
def delete(self):
    """Delete the Card"""
    return self.endpoints.deleteCard(self.id)

diff

diff(other: PlankaModel[Schema]) -> Diff

Get a schema diff between two model schemas.

Note

Only matching keys are diffed. Any schema keys that are not in the source schema will not be checked in the target schema

Source code in src/plankapy/v2/models/_base.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def diff(self, other: PlankaModel[Schema]) -> Diff:
    """Get a schema diff between two model schemas.

    Note:
        Only matching keys are diffed. Any schema keys that are not in the source schema 
        will not be checked in the target schema
    """
    return {
        k: (source, delta) 
        for k, source in self.schema
        if k in other.schema
        and (delta := other.schema[k]) 
        and delta != source
    }

duplicate

duplicate(position: Position = 'top', *, name: str | None = None) -> Card

Duplicate the card in the current List

PARAMETER DESCRIPTION

position

The position to place the new Card in (default: top)

TYPE: Position DEFAULT: 'top'

name

An optional name to give the new Card (default {name} (copy))

TYPE: str | None DEFAULT: None

Source code in src/plankapy/v2/models/card.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def duplicate(self, position: Position = 'top', *, name: str|None=None) -> Card:
    """Duplicate the card in the current List

    Args:
        position (Position): The position to place the new Card in (default: `top`)
        name (str|None): An optional name to give the new Card (default `{name} (copy)`)
    """
    position = get_position(self.list.cards, position)
    return Card(
        self.endpoints.duplicateCard(
            self.id, 
            position=position, 
            name=name or f'{self.name} (copy)'
        )['item'], 
        self.session
    )

get_field_values

get_field_values(*, with_groups: bool = False) -> dict[str, Any]

Get a mapping of CustomFields to CustomFieldValues

PARAMETER DESCRIPTION

with_groups

If set to True, return a nested dict with {group: {field: value, ...}, ...}, otherwise {field: value ...} (default: False)

TYPE: bool DEFAULT: False

Source code in src/plankapy/v2/models/card.py
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
def get_field_values(self, *, with_groups: bool=False) -> dict[str, Any]:
    """Get a mapping of CustomFields to CustomFieldValues

    Args:
        with_groups (bool): If set to `True`, 
            return a nested dict with `{group: {field: value, ...}, ...}`, otherwise `{field: value ...}` (default: `False`)
    """
    if with_groups:
        return {
            cfg.name: {cfv.custom_field.name: cfv.content for cfv in cfg.custom_field_values}
            for cfg in self.custom_field_groups
        }
    else:
        return {
            cfv.custom_field.name: cfv.content
            for cfv in self.custom_field_values
        }

move

move(list: List, position: Position = 'top') -> Card

Move the card to a new list (default to top of new list)

Source code in src/plankapy/v2/models/card.py
345
346
347
348
349
350
351
352
def move(self, list: List, position: Position = 'top') -> Card:
    """Move the card to a new list (default to top of new list)"""
    self.update(
        listId=list.id, 
        boardId=list.board.id, 
        position=get_position(list.cards, position),
    )
    return self

read_notifications

read_notifications() -> list[Notification]

Read all the current User's Notifications for the Card

Source code in src/plankapy/v2/models/card.py
290
291
292
293
@model_list
def read_notifications(self) -> list[Notification]:
    """Read all the current User's Notifications for the Card"""
    return [Notification(n, self.session) for n in self.endpoints.readCardNotifications(self.id)['included']['notifications']]

remove_label

remove_label(label: Label) -> Label | None

Remove the Label from the Card

PARAMETER DESCRIPTION

label

The label to remove (must be associated with the Card)

TYPE: Label

Source code in src/plankapy/v2/models/card.py
659
660
661
662
663
664
665
666
667
668
def remove_label(self, label: Label) -> Label | None:
    """Remove the Label from the Card

    Args:
        label (Label): The label to remove (must be associated with the Card)
    """
    for card_label in self.card_labels:
        if card_label.label == label:
            card_label.delete()
            return label

remove_labels

remove_labels(labels: Sequence[Label]) -> list[Label]

Remove the Label from the Card

PARAMETER DESCRIPTION

labels

The labels to remove (must be associated with the Card)

TYPE: Sequence[Label]

Source code in src/plankapy/v2/models/card.py
670
671
672
673
674
675
676
677
@model_list
def remove_labels(self, labels: Sequence[Label]) -> list[Label]:
    """Remove the Label from the Card

    Args:
        labels (Sequence[Label]): The labels to remove (must be associated with the Card)
    """
    return [removed for label in labels if (removed := self.remove_label(label))]

remove_member

remove_member(user: User) -> User | None

Remove a User member from the Card

PARAMETER DESCRIPTION

user

The User to remove

TYPE: User

RETURNS DESCRIPTION
User | None

The removed User or None if tha User was not a member

Source code in src/plankapy/v2/models/card.py
574
575
576
577
578
579
580
581
582
583
584
585
586
def remove_member(self, user: User) -> User | None:
    """Remove a User member from the Card

    Args:
        user (User): The User to remove

    Returns:
        (User | None): The removed User or None if tha User was not a member
    """
    for cm in self.card_memberships:
        if cm.user == user:
            cm.delete()
            return user

remove_members

remove_members(users: Sequence[User]) -> list[User]

Remove multiple members from a Card

PARAMETER DESCRIPTION

users

The Users to remove from the Card

TYPE: Sequence[User]

RETURNS DESCRIPTION
list[User]

list[User]: The Users that were removed from the card

Note

If a User in the users sequence is not a member of the card, They will be excluded from this list

Source code in src/plankapy/v2/models/card.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
@model_list
def remove_members(self, users: Sequence[User]) -> list[User]:
    """Remove multiple members from a Card

    Args:
        users (Sequence[User]): The Users to remove from the Card

    Returns:
        list[User]: The Users that were removed from the card

    Note:
        If a User in the `users` sequence is not a member of the card, 
        They will be excluded from this list
    """
    return [
        user
        for user in users
        if self.remove_member(user) is not None
    ]

restore

restore(position: Position = 'top') -> Card

Restore the Card from arcive/trash to its previous list

Source code in src/plankapy/v2/models/card.py
371
372
373
374
375
def restore(self, position: Position='top') -> Card:
    """Restore the Card from arcive/trash to its previous list"""
    if self.prev_list is not None:
        self.move(self.prev_list, position)
    return self

sync

sync()

Sync the Card with the Planka server

Source code in src/plankapy/v2/models/card.py
271
272
273
def sync(self):
    """Sync the Card with the Planka server"""
    self.schema = self.endpoints.getCard(self.id)['item']

update

update(**kwargs: Unpack[Request_updateCard])

Update the Card

Note

dueDate can be set using ISO 8601 string or datetime object

Source code in src/plankapy/v2/models/card.py
275
276
277
278
279
280
281
282
283
284
def update(self, **kwargs: Unpack[paths.Request_updateCard]):
    """Update the Card

    Note:
        dueDate can be set using ISO 8601 string or datetime object
    """
    # Convert the dueDate to a iso string if a datetime is passed
    if 'dueDate' in kwargs:
        kwargs['dueDate'] = str(kwargs['dueDate'])
    self.schema = self.endpoints.updateCard(self.id, **kwargs)['item']

Stopwatch

Stopwatch(card: Card)

Python interface for Planka Stopwatches

METHOD DESCRIPTION
start

Start the stopwatch and return the current datetime (None if the stopwatch is started)

stop

Stop a running stopwatch and return the time it was last started

update

Update the stopwatch

ATTRIBUTE DESCRIPTION
enabled

If the stopwatch is enabled for the card (visible on front)

is_running

If the stopwatch is currently running

TYPE: bool

last_started

The time a running stopwatch was started (None if the stopwatch is stopped)

TYPE: datetime | None

Source code in src/plankapy/v2/models/card.py
753
754
755
def __init__(self, card: Card) -> None:
    self.card = card
    self.tz = card.session.timezone

enabled property writable

enabled

If the stopwatch is enabled for the card (visible on front)

is_running property

is_running: bool

If the stopwatch is currently running

last_started property

last_started: datetime | None

The time a running stopwatch was started (None if the stopwatch is stopped)

start

start() -> datetime | None

Start the stopwatch and return the current datetime (None if the stopwatch is started)

Source code in src/plankapy/v2/models/card.py
806
807
808
809
810
def start(self) -> datetime | None:
    """Start the stopwatch and return the current datetime (None if the stopwatch is started)"""
    if self.is_running:
       return None
    return datetime.now(tz=self.card.session.timezone)

stop

stop() -> datetime | None

Stop a running stopwatch and return the time it was last started

Source code in src/plankapy/v2/models/card.py
812
813
814
815
816
def stop(self) -> datetime | None:
    """Stop a running stopwatch and return the time it was last started"""
    started = self.last_started
    self.update(started_at=None)
    return started

update

update(started_at: datetime | str | None = None, total: timedelta | int | None = None) -> None

Update the stopwatch

Source code in src/plankapy/v2/models/card.py
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
def update(self, started_at: datetime | str | None=None, total: timedelta | int | None=None) -> None:
    """Update the stopwatch"""        
    current = self.schema
    if started_at:
        current['startedAt'] = str(
            dttoiso(started_at, default_timezone=self.tz) 
            if isinstance(started_at, datetime) 
            else started_at
        )
    if total:
        current['total'] = int(round( # Round seconds
            total.total_seconds() 
            if isinstance(total, timedelta) 
            else total
        ))
    self.card.update(stopwatch=current)