From 7f2d905eee0840dba7cd6390c84a78a6b002edb4 Mon Sep 17 00:00:00 2001 From: Andrey Krayushkin Date: Mon, 29 Jun 2026 17:57:54 +0300 Subject: [PATCH] Added meetings public api Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 ++ README.md | 65 +++++++++++++++++++++ pyrus/client.py | 116 ++++++++++++++++++++++++++++++++++++++ pyrus/models/entities.py | 112 ++++++++++++++++++++++++++++++++++++ pyrus/models/requests.py | 105 ++++++++++++++++++++++++++++++++++ pyrus/models/responses.py | 102 +++++++++++++++++++++++++++++++++ pyrus/version.py | 2 +- 7 files changed, 506 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4003774..a8770a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.50.0] - 2026-06-29 +### Added +- Meetings API endpoints +- 'meeting_action' property in TaskComment entity + ## [2.49.1] - 2026-06-24 ### Fixed - Form register sort: fixed sort type check ('tsk' -> 'id'), so sorting by task id no longer raises an error diff --git a/README.md b/README.md index f16e804..3c4e2b1 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,71 @@ request = pyrus.models.requests.UpdateKnowledgeBasePermissionsRequest( response = pyrus_client.update_knowledge_base_permissions("KoSjtyL9EWm", request) ``` +## Meetings + +* Create a meeting linked to one or more tasks: + +```python +request = pyrus.models.requests.CreateMeetingRequest( + type="zoom", + start_time=datetime.datetime(2026, 6, 25, 14, 0, 0), + duration_minutes=60, + title="Sync up", + note="Quarterly planning", + member_ids=[1733, 2434], + meeting_room_ids=[42], + task_ids=[10501]) +meeting = pyrus_client.create_meeting(request) +``` + +* Get a meeting: + +```python +meeting = pyrus_client.get_meeting(7821) +``` + +* Update a meeting (full-replace of parameters and participants): + +```python +request = pyrus.models.requests.UpdateMeetingRequest( + type="offline", + start_time=datetime.datetime(2026, 6, 25, 15, 0, 0), + duration_minutes=45, + title="Sync up (rescheduled)", + member_ids=[1733], + meeting_room_ids=[42]) +meeting = pyrus_client.update_meeting(7821, request) +``` + +* Bulk add/remove task links: + +```python +request = pyrus.models.requests.UpdateMeetingTasksRequest( + add=[10502, 10503], + remove=[10501]) +meeting = pyrus_client.update_meeting_tasks(7821, request) +``` + +* Change the current user's status in the meeting: + +```python +request = pyrus.models.requests.UpdateMeetingMyStatusRequest(status="going") +meeting = pyrus_client.update_meeting_my_status(7821, request) +``` + +* Delete a meeting: + +```python +response = pyrus_client.delete_meeting(7821) +``` + +* Get meeting rooms of the organization: + +```python +response = pyrus_client.get_meeting_rooms() +rooms = response.meeting_rooms +``` + ## Support If you have any questions or comments please send an email to support@pyrus.com diff --git a/pyrus/client.py b/pyrus/client.py index 6ef9904..fea3ecb 100644 --- a/pyrus/client.py +++ b/pyrus/client.py @@ -718,6 +718,122 @@ def update_knowledge_base_permissions(self, entity_id, request): response = self._perform_put_request('/knowledgebase/{}/permissions'.format(entity_id), request) return resp.KnowledgeBasePermissionsResponse(**response) + def create_meeting(self, request): + """ + Create a meeting. This method returns the created meeting. + + Args: + request (:obj:`models.requests.CreateMeetingRequest`): Meeting data. + + Returns: + class:`models.responses.MeetingResponse` object + """ + if not isinstance(request, req.CreateMeetingRequest): + raise TypeError('request must be an instance of models.requests.CreateMeetingRequest') + + response = self._perform_post_request('/meetings', request) + return resp.MeetingResponse(**response) + + def get_meeting(self, meeting_id): + """ + Get a meeting. + + Args: + meeting_id (:obj:`int`): Meeting id + + Returns: + class:`models.responses.MeetingResponse` object + """ + if not isinstance(meeting_id, int): + raise TypeError('meeting_id must be an instance of int') + + response = self._perform_get_request('/meetings/{}'.format(meeting_id)) + return resp.MeetingResponse(**response) + + def update_meeting(self, meeting_id, request): + """ + Update a meeting (full-replace of parameters and participants). + Task links and the personal status are managed by separate methods. + + Args: + meeting_id (:obj:`int`): Meeting id + request (:obj:`models.requests.UpdateMeetingRequest`): Meeting data. + + Returns: + class:`models.responses.MeetingResponse` object + """ + if not isinstance(meeting_id, int): + raise TypeError('meeting_id must be an instance of int') + if not isinstance(request, req.UpdateMeetingRequest): + raise TypeError('request must be an instance of models.requests.UpdateMeetingRequest') + + response = self._perform_put_request('/meetings/{}'.format(meeting_id), request) + return resp.MeetingResponse(**response) + + def update_meeting_tasks(self, meeting_id, request): + """ + Bulk add/remove of meeting task links. + + Args: + meeting_id (:obj:`int`): Meeting id + request (:obj:`models.requests.UpdateMeetingTasksRequest`): Task link changes. + + Returns: + class:`models.responses.MeetingResponse` object + """ + if not isinstance(meeting_id, int): + raise TypeError('meeting_id must be an instance of int') + if not isinstance(request, req.UpdateMeetingTasksRequest): + raise TypeError('request must be an instance of models.requests.UpdateMeetingTasksRequest') + + response = self._perform_post_request('/meetings/{}/tasks'.format(meeting_id), request) + return resp.MeetingResponse(**response) + + def update_meeting_my_status(self, meeting_id, request): + """ + Change the current user's status in the meeting. + + Args: + meeting_id (:obj:`int`): Meeting id + request (:obj:`models.requests.UpdateMeetingMyStatusRequest`): Status change. + + Returns: + class:`models.responses.MeetingResponse` object + """ + if not isinstance(meeting_id, int): + raise TypeError('meeting_id must be an instance of int') + if not isinstance(request, req.UpdateMeetingMyStatusRequest): + raise TypeError('request must be an instance of models.requests.UpdateMeetingMyStatusRequest') + + response = self._perform_put_request('/meetings/{}/my-status'.format(meeting_id), request) + return resp.MeetingResponse(**response) + + def delete_meeting(self, meeting_id): + """ + Delete a meeting. + + Args: + meeting_id (:obj:`int`): Meeting id + + Returns: + class:`models.responses.DeleteMeetingResponse` object + """ + if not isinstance(meeting_id, int): + raise TypeError('meeting_id must be an instance of int') + + response = self._perform_delete_request('/meetings/{}'.format(meeting_id)) + return resp.DeleteMeetingResponse(**response) + + def get_meeting_rooms(self): + """ + Get the list of meeting rooms in the user's organization. + + Returns: + class:`models.responses.MeetingRoomsResponse` object + """ + response = self._perform_get_request('/meetings/meeting-rooms') + return resp.MeetingRoomsResponse(**response) + def _auth(self): url = self._create_auth_url('/auth') headers = { diff --git a/pyrus/models/entities.py b/pyrus/models/entities.py index 65896a7..b5ec7fd 100644 --- a/pyrus/models/entities.py +++ b/pyrus/models/entities.py @@ -529,6 +529,7 @@ class TaskComment: due_date (:obj:`datetime`): Task due date due (:obj:`datetime`): Task due date with time duration (:obj:`int`): Task duration in minutes + meeting_action (:obj:`models.entities.MeetingAction`): Meeting action description attached to the comment Attributes(Simple Task comment): reassign_to (:obj:`models.entities.Person`): Person to whom the task was reassigned participants_added (:obj:`list` of :obj:`models.entities.Person`): List of participants added to the task @@ -582,6 +583,7 @@ class TaskComment: reply_note_id = None skip_notification = None skip_auto_reopen = None + meeting_action = None @property def flat_field_updates(self): @@ -675,6 +677,8 @@ def __init__(self, **kwargs): self.skip_notification = kwargs['skip_notification'] if 'skip_auto_reopen' in kwargs: self.skip_auto_reopen = kwargs['skip_auto_reopen'] + if 'meeting_action' in kwargs and kwargs['meeting_action'] is not None: + self.meeting_action = MeetingAction(**kwargs['meeting_action']) class TaskStep: """ @@ -1511,6 +1515,114 @@ def __init__(self, **kwargs): self.deleted = kwargs['deleted'] +class MeetingPersonInfo: + """ + Meeting person info + + Attributes: + id (:obj:`int`): Person id + first_name (:obj:`str`): Person first name + last_name (:obj:`str`): Person last name + email (:obj:`str`): Person email (null for cross-organization users and roles) + """ + + id = None + first_name = None + last_name = None + email = None + + def __init__(self, **kwargs): + if 'id' in kwargs: + self.id = kwargs['id'] + if 'first_name' in kwargs: + self.first_name = kwargs['first_name'] + if 'last_name' in kwargs: + self.last_name = kwargs['last_name'] + if 'email' in kwargs: + self.email = kwargs['email'] + + +class MeetingMember: + """ + Meeting member + + Attributes: + person (:obj:`models.entities.MeetingPersonInfo`): Member person info + status (:obj:`str`): Member status. One of 'undefined', 'going', 'maybe', 'not_going', 'going_virtually', 'going_to_meeting_room' + """ + + person = None + status = None + + def __init__(self, **kwargs): + if 'person' in kwargs: + self.person = MeetingPersonInfo(**kwargs['person']) + if 'status' in kwargs: + self.status = kwargs['status'] + + +class MeetingRoom: + """ + Meeting room + + Attributes: + id (:obj:`int`): Meeting room id + name (:obj:`str`): Meeting room name + """ + + id = None + name = None + + def __init__(self, **kwargs): + if 'id' in kwargs: + self.id = kwargs['id'] + if 'name' in kwargs: + self.name = kwargs['name'] + + +class MeetingAction: + """ + Meeting action description attached to task comments + + Attributes: + action_type (:obj:`str`): One of 'added', 'updated', 'deleted' + meeting_id (:obj:`int`): Meeting id + type (:obj:`str`): Meeting type ('offline', 'zoom', 'google_meet', 'yandex_telemost') + start_time (:obj:`datetime`): Meeting start time + duration_minutes (:obj:`int`): Meeting duration in minutes + title (:obj:`str`): Meeting title + note (:obj:`str`): Meeting note + join_parameters (:obj:`models.entities.MeetingJoinParameters`): Meeting join parameters (null for offline) + """ + + action_type = None + meeting_id = None + type = None + start_time = None + duration_minutes = None + title = None + note = None + join_parameters = None + + def __init__(self, **kwargs): + if 'action_type' in kwargs: + self.action_type = kwargs['action_type'] + if 'meeting_id' in kwargs: + self.meeting_id = kwargs['meeting_id'] + if 'type' in kwargs: + self.type = kwargs['type'] + if 'start_time' in kwargs and kwargs['start_time'] is not None: + self.start_time = _set_utc_timezone(datetime.strptime(kwargs['start_time'], constants.DATE_TIME_FORMAT)) + if 'duration_minutes' in kwargs: + self.duration_minutes = kwargs['duration_minutes'] + if 'title' in kwargs: + self.title = kwargs['title'] + if 'note' in kwargs: + self.note = kwargs['note'] + if 'join_parameters' in kwargs and kwargs['join_parameters'] is not None: + self.join_parameters = MeetingJoinParameters(**kwargs['join_parameters']) + + class TaskGroup: """ Task group diff --git a/pyrus/models/requests.py b/pyrus/models/requests.py index 6f048ef..bf93127 100644 --- a/pyrus/models/requests.py +++ b/pyrus/models/requests.py @@ -971,3 +971,108 @@ def __init__(self, inherit=None, readers=None, editors=None): self.inherit = inherit self.readers = readers self.editors = editors + + +class CreateMeetingRequest: + """ + CreateMeetingRequest + + Args: + type (:obj:`str`): Meeting type ('offline', 'zoom', 'google_meet', 'yandex_telemost') + start_time (:obj:`datetime`): Meeting start time + duration_minutes (:obj:`int`): Meeting duration in minutes + title (:obj:`str`): Meeting title + note (:obj:`str`, optional): Meeting note + member_ids (:obj:`list` of :obj:`int`, optional): Meeting member ids + meeting_room_ids (:obj:`list` of :obj:`int`, optional): Meeting room ids + task_ids (:obj:`list` of :obj:`int`, optional): Ids of tasks to link the meeting to + should_be_shared_to_external_person (:obj:`bool`, optional): Whether the meeting should be shared to external person by email + """ + + def __init__(self, type, start_time, duration_minutes, title, + note=None, member_ids=None, meeting_room_ids=None, task_ids=None, + should_be_shared_to_external_person=None): + if not isinstance(type, str): + raise TypeError('type must be str') + if not isinstance(start_time, datetime): + raise TypeError('start_time must be datetime') + if not isinstance(duration_minutes, int): + raise TypeError('duration_minutes must be int') + if not isinstance(title, str): + raise TypeError('title must be str') + + self.type = type + self.start_time = datetime.strftime(start_time, constants.DATE_TIME_FORMAT) + self.duration_minutes = duration_minutes + self.title = title + self.note = note + self.member_ids = member_ids + self.meeting_room_ids = meeting_room_ids + self.task_ids = task_ids + self.should_be_shared_to_external_person = should_be_shared_to_external_person + + +class UpdateMeetingRequest: + """ + UpdateMeetingRequest. Full-replace of meeting parameters and participants. + Task links and the personal status are managed by separate endpoints. + + Args: + type (:obj:`str`): Meeting type ('offline', 'zoom', 'google_meet', 'yandex_telemost') + start_time (:obj:`datetime`): Meeting start time + duration_minutes (:obj:`int`): Meeting duration in minutes + title (:obj:`str`): Meeting title + note (:obj:`str`, optional): Meeting note + member_ids (:obj:`list` of :obj:`int`, optional): Meeting member ids + meeting_room_ids (:obj:`list` of :obj:`int`, optional): Meeting room ids + should_be_shared_to_external_person (:obj:`bool`, optional): Whether the meeting should be shared to external person by email + """ + + def __init__(self, type, start_time, duration_minutes, title, + note=None, member_ids=None, meeting_room_ids=None, + should_be_shared_to_external_person=None): + if not isinstance(type, str): + raise TypeError('type must be str') + if not isinstance(start_time, datetime): + raise TypeError('start_time must be datetime') + if not isinstance(duration_minutes, int): + raise TypeError('duration_minutes must be int') + if not isinstance(title, str): + raise TypeError('title must be str') + + self.type = type + self.start_time = datetime.strftime(start_time, constants.DATE_TIME_FORMAT) + self.duration_minutes = duration_minutes + self.title = title + self.note = note + self.member_ids = member_ids + self.meeting_room_ids = meeting_room_ids + self.should_be_shared_to_external_person = should_be_shared_to_external_person + + +class UpdateMeetingTasksRequest: + """ + UpdateMeetingTasksRequest. Bulk add/remove of meeting task links. + + Args: + add (:obj:`list` of :obj:`int`, optional): Task ids to link to the meeting + remove (:obj:`list` of :obj:`int`, optional): Task ids to unlink from the meeting + """ + + def __init__(self, add=None, remove=None): + self.add = add + self.remove = remove + + +class UpdateMeetingMyStatusRequest: + """ + UpdateMeetingMyStatusRequest. Change the current user's status in the meeting. + + Args: + status (:obj:`str`): One of 'undefined', 'going', 'maybe', 'not_going', 'going_virtually', 'going_to_meeting_room' + """ + + def __init__(self, status): + if not isinstance(status, str): + raise TypeError('status must be str') + self.status = status diff --git a/pyrus/models/responses.py b/pyrus/models/responses.py index 1e35527..41dd542 100644 --- a/pyrus/models/responses.py +++ b/pyrus/models/responses.py @@ -2,7 +2,9 @@ # pylint: disable=R0903 # pylint: disable=too-many-instance-attributes +from datetime import datetime from . import entities +from . import constants class BaseResponse: @@ -743,3 +745,103 @@ def __init__(self, **kwargs): if 'editors' in kwargs: self.editors = [entities.KnowledgeBasePersonInfo(**e) for e in kwargs['editors']] super(KnowledgeBasePermissionsResponse, self).__init__(**kwargs) + + +class MeetingResponse(BaseResponse): + """ + MeetingResponse + + Attributes: + id (:obj:`int`): Meeting id + type (:obj:`str`): Meeting type ('offline', 'zoom', 'google_meet', 'yandex_telemost') + start_time (:obj:`datetime`): Meeting start time + duration_minutes (:obj:`int`): Meeting duration in minutes + title (:obj:`str`): Meeting title + note (:obj:`str`): Meeting note + creator (:obj:`models.entities.MeetingPersonInfo`): Meeting creator + task_ids (:obj:`list` of :obj:`int`): Ids of tasks linked to the meeting + members (:obj:`list` of :obj:`models.entities.MeetingMember`): Meeting members + meeting_rooms (:obj:`list` of :obj:`models.entities.MeetingRoom`): Meeting rooms + join_parameters (:obj:`models.entities.MeetingJoinParameters`): Meeting join parameters (null for offline) + shared_calendar_event_id (:obj:`str`): Unique identifier for external users + should_be_shared_to_external_person (:obj:`bool`): Whether the meeting should be shared to external person by email + """ + __doc__ += BaseResponse.__doc__ + + id = None + type = None + start_time = None + duration_minutes = None + title = None + note = None + creator = None + task_ids = None + members = None + meeting_rooms = None + join_parameters = None + shared_calendar_event_id = None + should_be_shared_to_external_person = None + + def __init__(self, **kwargs): + if 'id' in kwargs: + self.id = kwargs['id'] + if 'type' in kwargs: + self.type = kwargs['type'] + if 'start_time' in kwargs and kwargs['start_time'] is not None: + self.start_time = entities._set_utc_timezone( + datetime.strptime(kwargs['start_time'], constants.DATE_TIME_FORMAT)) + if 'duration_minutes' in kwargs: + self.duration_minutes = kwargs['duration_minutes'] + if 'title' in kwargs: + self.title = kwargs['title'] + if 'note' in kwargs: + self.note = kwargs['note'] + if 'creator' in kwargs and kwargs['creator'] is not None: + self.creator = entities.MeetingPersonInfo(**kwargs['creator']) + if 'task_ids' in kwargs: + self.task_ids = list(kwargs['task_ids']) if kwargs['task_ids'] is not None else None + if 'members' in kwargs and kwargs['members'] is not None: + self.members = [entities.MeetingMember(**m) for m in kwargs['members']] + if 'meeting_rooms' in kwargs and kwargs['meeting_rooms'] is not None: + self.meeting_rooms = [entities.MeetingRoom(**r) for r in kwargs['meeting_rooms']] + if 'join_parameters' in kwargs and kwargs['join_parameters'] is not None: + self.join_parameters = entities.MeetingJoinParameters(**kwargs['join_parameters']) + if 'shared_calendar_event_id' in kwargs: + self.shared_calendar_event_id = kwargs['shared_calendar_event_id'] + if 'should_be_shared_to_external_person' in kwargs: + self.should_be_shared_to_external_person = kwargs['should_be_shared_to_external_person'] + super(MeetingResponse, self).__init__(**kwargs) + + +class MeetingRoomsResponse(BaseResponse): + """ + MeetingRoomsResponse + + Attributes: + meeting_rooms (:obj:`list` of :obj:`models.entities.MeetingRoom`): List of meeting rooms + """ + __doc__ += BaseResponse.__doc__ + + meeting_rooms = None + + def __init__(self, **kwargs): + if 'meeting_rooms' in kwargs and kwargs['meeting_rooms'] is not None: + self.meeting_rooms = [entities.MeetingRoom(**r) for r in kwargs['meeting_rooms']] + super(MeetingRoomsResponse, self).__init__(**kwargs) + + +class DeleteMeetingResponse(BaseResponse): + """ + DeleteMeetingResponse + + Attributes: + deleted (:obj:`bool`): Whether the meeting was deleted + """ + __doc__ += BaseResponse.__doc__ + + deleted = None + + def __init__(self, **kwargs): + if 'deleted' in kwargs: + self.deleted = kwargs['deleted'] + super(DeleteMeetingResponse, self).__init__(**kwargs) diff --git a/pyrus/version.py b/pyrus/version.py index 9db5518..61005a7 100644 --- a/pyrus/version.py +++ b/pyrus/version.py @@ -1 +1 @@ -VERSION = "2.49.1" +VERSION = "2.50.0"