Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
116 changes: 116 additions & 0 deletions pyrus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
112 changes: 112 additions & 0 deletions pyrus/models/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет проверки на None, в отличие от MeetingResponse.start_time, где сделано if 'start_time' in kwargs and kwargs['start_time'] is not None. Если сервер для action_type='deleted' (или иного) пришлёт start_time: null, то strptime(None, ...) упадёт с TypeError. А так как meeting_action парсится при разборе любого комментария задачи, это может уронить get_task/get_comments. Предлагаю выровнять с MeetingResponse:

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
Expand Down
Loading