From 64a0e028bf0b09046fc896363b207bdfa121e13b Mon Sep 17 00:00:00 2001 From: Subash Pradhan Date: Fri, 13 Dec 2024 07:54:25 +0100 Subject: [PATCH 1/2] Scheduler API support (#393) --- CHANGELOG.md | 4 + nylas/client.py | 11 + nylas/handler/api_resources.py | 18 + nylas/models/availability.py | 4 +- nylas/models/messages.py | 2 - nylas/models/scheduler.py | 527 +++++++++++++++++++++++++ nylas/resources/bookings.py | 176 +++++++++ nylas/resources/configurations.py | 160 ++++++++ nylas/resources/scheduler.py | 42 ++ nylas/resources/sessions.py | 56 +++ tests/resources/test_bookings.py | 116 ++++++ tests/resources/test_calendars.py | 2 +- tests/resources/test_configurations.py | 234 +++++++++++ tests/resources/test_sessions.py | 45 +++ 14 files changed, 1392 insertions(+), 5 deletions(-) create mode 100644 nylas/models/scheduler.py create mode 100644 nylas/resources/bookings.py create mode 100644 nylas/resources/configurations.py create mode 100644 nylas/resources/scheduler.py create mode 100644 nylas/resources/sessions.py create mode 100644 tests/resources/test_bookings.py create mode 100644 tests/resources/test_configurations.py create mode 100644 tests/resources/test_sessions.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ecbb7e..f66fc440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ nylas-python Changelog ====================== +Unreleased +-------------- +* Add support for Scheduler APIs + v6.4.0 ---------------- * Add support for from field for sending messages diff --git a/nylas/client.py b/nylas/client.py index 00b5ad8c..349c55fb 100644 --- a/nylas/client.py +++ b/nylas/client.py @@ -13,6 +13,7 @@ from nylas.resources.contacts import Contacts from nylas.resources.drafts import Drafts from nylas.resources.grants import Grants +from nylas.resources.scheduler import Scheduler class Client: @@ -169,3 +170,13 @@ def webhooks(self) -> Webhooks: The Webhooks API. """ return Webhooks(self.http_client) + + @property + def scheduler(self) -> Scheduler: + """ + Access the Scheduler API. + + Returns: + The Scheduler API. + """ + return Scheduler(self.http_client) diff --git a/nylas/handler/api_resources.py b/nylas/handler/api_resources.py index ca38bce2..35c6dce2 100644 --- a/nylas/handler/api_resources.py +++ b/nylas/handler/api_resources.py @@ -75,6 +75,24 @@ def update( return Response.from_dict(response_json, response_type) +class UpdatablePatchApiResource(Resource): + def patch( + self, + path, + response_type, + headers=None, + query_params=None, + request_body=None, + method="PATCH", + overrides=None, + ): + response_json = self._http_client._execute( + method, path, headers, query_params, request_body, overrides=overrides + ) + + return Response.from_dict(response_json, response_type) + + class DestroyableApiResource(Resource): def destroy( self, diff --git a/nylas/models/availability.py b/nylas/models/availability.py index 005977d3..68497f38 100644 --- a/nylas/models/availability.py +++ b/nylas/models/availability.py @@ -87,7 +87,7 @@ class AvailabilityRules(TypedDict): default_open_hours: A default set of open hours to apply to all participants. You can overwrite these open hours for individual participants by specifying open_hours on the participant object. - round_robin_event_id: The ID on events that Nylas considers when calculating the order of + round_robin_group_id: The ID on events that Nylas considers when calculating the order of round-robin participants. This is used for both max-fairness and max-availability methods. """ @@ -95,7 +95,7 @@ class AvailabilityRules(TypedDict): availability_method: NotRequired[AvailabilityMethod] buffer: NotRequired[MeetingBuffer] default_open_hours: NotRequired[List[OpenHours]] - round_robin_event_id: NotRequired[str] + round_robin_group_id: NotRequired[str] class AvailabilityParticipant(TypedDict): diff --git a/nylas/models/messages.py b/nylas/models/messages.py index 12da517d..7d5d405e 100644 --- a/nylas/models/messages.py +++ b/nylas/models/messages.py @@ -132,7 +132,6 @@ class Message: class FindMessageQueryParams(TypedDict): - """ Query parameters for finding a message. @@ -144,7 +143,6 @@ class FindMessageQueryParams(TypedDict): class UpdateMessageRequest(TypedDict): - """ Request payload for updating a message. diff --git a/nylas/models/scheduler.py b/nylas/models/scheduler.py new file mode 100644 index 00000000..7b20b565 --- /dev/null +++ b/nylas/models/scheduler.py @@ -0,0 +1,527 @@ +from dataclasses import dataclass +from typing import Dict, Optional, List + +from dataclasses_json import dataclass_json +from typing_extensions import TypedDict, NotRequired, Literal +from nylas.models.events import Conferencing +from nylas.models.availability import AvailabilityRules, OpenHours + +BookingType = Literal["booking", "organizer-confirmation"] +BookingReminderType = Literal["email", "webhook"] +BookingRecipientType = Literal["host", "guest", "all"] +EmailLanguage = Literal["en", "es", "fr", "de", "nl", "sv", "ja", "zh"] +AdditionalFieldType = Literal[ + "text", + "multi_line_text", + "email", + "phone_number", + "dropdown", + "date", + "checkbox", + "radio_button", +] +AdditonalFieldOptionsType = Literal[ + "text", "email", "phone_number", "date", "checkbox", "radio_button" +] + + +@dataclass_json +@dataclass +class BookingConfirmedTemplate: + """ + Class representation of booking confirmed template settings. + + Attributes: + title: The title to replace the default 'Booking Confirmed' title. + body: The additional body to be appended after the default body. + """ + + title: Optional[str] = None + body: Optional[str] = None + + +@dataclass_json +@dataclass +class EmailTemplate: + """ + Class representation of email template settings. + + Attributes: + logo: The URL to a custom logo that appears at the top of the booking email. + booking_confirmed: Configurable settings specifically for booking confirmed emails. + """ + + # logo: Optional[str] = None + booking_confirmed: Optional[BookingConfirmedTemplate] = None + + +@dataclass_json +@dataclass +class AdditionalField: + """ + Class representation of an additional field. + + Atributes: + label: The text label to be displayed in the Scheduler UI. + type: The field type. Supported values are text, multi_line_text, + email, phone_number, dropdown, date, checkbox, and radio_button + required: Whether the field is required to be filled out by the guest when booking an event. + pattern: A regular expression pattern that the value of the field must match. + order: The order in which the field will be displayed in the Scheduler UI. + Fields with lower order values will be displayed first. + options: A list of options for the dropdown or radio_button types. + This field is required for the dropdown and radio_button types. + """ + + label: str + type: AdditionalFieldType + required: bool + pattern: Optional[str] = None + order: Optional[int] = None + options: Optional[AdditonalFieldOptionsType] = None + + +@dataclass_json +@dataclass +class SchedulerSettings: + """ + Class representation of scheduler settings. + + Attributes: + additional_fields: Definitions for additional fields to be displayed in the Scheduler UI. + available_days_in_future: Number of days in the future that Scheduler is available for scheduling events. + min_booking_notice: Minimum number of minutes in the future that a user can make a new booking. + min_cancellation_notice: Minimum number of minutes before a booking can be cancelled. + cancellation_policy: A message about the cancellation policy to display when booking an event. + rescheduling_url: The URL used to reschedule bookings. + cancellation_url: The URL used to cancel bookings. + organizer_confirmation_url: The URL used to confirm or cancel pending bookings. + confirmation_redirect_url: The custom URL to redirect to once the booking is confirmed. + hide_rescheduling_options: Whether the option to reschedule an event + is hidden in booking confirmations and notifications. + hide_cancellation_options: Whether the option to cancel an event + is hidden in booking confirmations and notifications. + hide_additional_guests: Whether to hide the additional guests field on the scheduling page. + email_template: Configurable settings for booking emails. + """ + + additional_fields: Optional[Dict[str, AdditionalField]] = None + available_days_in_future: Optional[int] = None + min_booking_notice: Optional[int] = None + min_cancellation_notice: Optional[int] = None + cancellation_policy: Optional[str] = None + rescheduling_url: Optional[str] = None + cancellation_url: Optional[str] = None + organizer_confirmation_url: Optional[str] = None + confirmation_redirect_url: Optional[str] = None + hide_rescheduling_options: Optional[bool] = None + hide_cancellation_options: Optional[bool] = None + hide_additional_guests: Optional[bool] = None + email_template: Optional[EmailTemplate] = None + + +@dataclass_json +@dataclass +class BookingReminder: + """ + Class representation of a booking reminder. + + Attributes: + type: The reminder type. + minutes_before_event: The number of minutes before the event to send the reminder. + recipient: The recipient of the reminder. + email_subject: The subject of the email reminder. + """ + + type: str + minutes_before_event: int + recipient: Optional[str] = None + email_subject: Optional[str] = None + + +@dataclass_json +@dataclass +class EventBooking: + """ + Class representation of an event booking. + + Attributes: + title: The title of the event. + description: The description of the event. + location: The location of the event. + timezone: The timezone for displaying times in confirmation email messages and reminders. + booking_type: The type of booking. + conferencing: Conference details for the event. + disable_emails: Whether Nylas sends email messages when an event is booked, cancelled, or rescheduled. + reminders: The list of reminders to send to participants before the event starts. + """ + + title: str + description: Optional[str] = None + location: Optional[str] = None + timezone: Optional[str] = None + booking_type: Optional[BookingType] = None + conferencing: Optional[Conferencing] = None + disable_emails: Optional[bool] = None + reminders: Optional[List[BookingReminder]] = None + + +@dataclass_json +@dataclass +class Availability: + """ + Class representation of availability settings. + + Attributes: + duration_minutes: The total number of minutes the event should last. + interval_minutes: The interval between meetings in minutes. + round_to: Nylas rounds each time slot to the nearest multiple of this number of minutes. + availability_rules: Availability rules for scheduling configuration. + """ + + duration_minutes: int + interval_minutes: Optional[int] = None + round_to: Optional[int] = None + availability_rules: Optional[AvailabilityRules] = None + + +@dataclass_json +@dataclass +class ParticipantBooking: + """ + Class representation of a participant booking. + + Attributes: + calendar_id: The calendar ID that the event is created in. + """ + + calendar_id: str + + +@dataclass_json +@dataclass +class ParticipantAvailability: + """ + Class representation of participant availability. + + Attributes: + calendar_ids: List of calendar IDs associated with the participant's email address. + open_hours: Open hours for this participant. The endpoint searches for free time slots during these open hours. + """ + + calendar_ids: List[str] + open_hours: Optional[List[OpenHours]] = None + + +@dataclass_json +@dataclass +class ConfigParticipant: + """ + Class representation of a booking participant. + + Attributes: + email: Participant's email address. + availability: Availability data for the participant. + booking: Booking data for the participant. + name: Participant's name. + is_organizer: Whether the participant is the organizer of the event. + timezone: The participant's timezone. + """ + + email: str + availability: ParticipantAvailability + booking: ParticipantBooking + name: Optional[str] = None + is_organizer: Optional[bool] = None + timezone: Optional[str] = None + + +@dataclass_json +@dataclass +class Configuration: + """ + Class representation of a scheduler configuration. + + Attributes: + participants: List of participants included in the scheduled event. + availability: Rules that determine available time slots for the event. + event_booking: Booking data for the event. + slug: Unique identifier for the Configuration object. + requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID. + scheduler: Settings for the Scheduler UI. + appearance: Appearance settings for the Scheduler UI. + """ + + id: str + participants: List[ConfigParticipant] + availability: Availability + event_booking: EventBooking + slug: Optional[str] = None + requires_session_auth: Optional[bool] = None + scheduler: Optional[SchedulerSettings] = None + appearance: Optional[Dict[str, str]] = None + + +class CreateConfigurationRequest(TypedDict): + """ + Interface of a Nylas create configuration request. + + Attributes: + participants: List of participants included in the scheduled event. + availability: Rules that determine available time slots for the event. + event_booking: Booking data for the event. + slug: Unique identifier for the Configuration object. + requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID. + scheduler: Settings for the Scheduler UI. + appearance: Appearance settings for the Scheduler UI. + """ + + participants: List[ConfigParticipant] + availability: Availability + event_booking: EventBooking + slug: NotRequired[str] + requires_session_auth: NotRequired[bool] + scheduler: NotRequired[SchedulerSettings] + appearance: NotRequired[Dict[str, str]] + + +class UpdateConfigurationRequest(TypedDict): + """ + Interface of a Nylas update configuration request. + + Attributes: + participants: List of participants included in the scheduled event. + availability: Rules that determine available time slots for the event. + event_booking: Booking data for the event. + slug: Unique identifier for the Configuration object. + requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID. + scheduler: Settings for the Scheduler UI. + appearance: Appearance settings for the Scheduler UI. + """ + participants: NotRequired[List[ConfigParticipant]] + availability: NotRequired[Availability] + event_booking: NotRequired[EventBooking] + slug: NotRequired[str] + requires_session_auth: NotRequired[bool] + scheduler: NotRequired[SchedulerSettings] + appearance: NotRequired[Dict[str, str]] + + +class CreateSessionRequest(TypedDict): + """ + Interface of a Nylas create session request. + + Attributes: + configuration_id: The ID of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true), + configuration_id is not required. + slug: The slug of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true) or using configurationId, + slug is not required. + time_to_live: The time-to-live in seconds for the session + """ + configuration_id: NotRequired[str] + slug: NotRequired[str] + time_to_live: NotRequired[int] + + +@dataclass_json +@dataclass +class Session: + """ + Class representation of a session. + + Attributes: + session_id: The ID of the session. + """ + + session_id: str + + +@dataclass_json +@dataclass +class BookingGuest: + """ + Class representation of a booking guest. + + Attributes: + email: The email address of the guest. + name: The name of the guest. + """ + + email: str + name: str + + +@dataclass_json +@dataclass +class BookingParticipant: + """ + Class representation of a booking participant. + + Attributes: + email: The email address of the participant to include in the booking. + """ + + email: str + + +@dataclass_json +@dataclass +class CreateBookingRequest: + """ + Class representation of a create booking request. + + Attributes: + start_time: The event's start time, in Unix epoch format. + end_time: The event's end time, in Unix epoch format. + guest: Details about the guest that is creating the booking. + participants: List of participant email addresses from the + Configuration object to include in the booking. + timezone: The guest's timezone that is used in email notifications. + email_language: The language of the guest email notifications. + additional_guests: List of additional guest email addresses to include in the booking. + additional_fields: Dictionary of additional field keys mapped to + values populated by the guest in the booking form. + """ + + start_time: int + end_time: int + guest: BookingGuest + participants: Optional[List[BookingParticipant]] = None + timezone: Optional[str] = None + email_language: Optional[EmailLanguage] = None + additional_guests: Optional[List[BookingGuest]] = None + additional_fields: Optional[Dict[str, str]] = None + + +@dataclass_json +@dataclass +class BookingOrganizer: + """ + Class representation of a booking organizer. + + Attributes: + email: The email address of the participant designated as the organizer of the event. + name: The name of the participant designated as the organizer of the event. + """ + + email: str + name: Optional[str] = None + + +BookingStatus = Literal["pending", "confirmed", "cancelled"] +ConfirmBookingStatus = Literal["confirm", "cancel"] + + +@dataclass_json +@dataclass +class Booking: + """ + Class representation of a booking. + + Attributes: + booking_id: The unique ID of the booking. + event_id: The unique ID of the event associated with the booking. + title: The title of the event. + organizer: The participant designated as the organizer of the event. + status: The current status of the booking. + description: The description of the event. + """ + + booking_id: str + event_id: str + title: str + organizer: BookingOrganizer + status: BookingStatus + description: Optional[str] = None + + +@dataclass_json +@dataclass +class ConfirmBookingRequest: + """ + Class representation of a confirm booking request. + + Attributes: + salt: The salt extracted from the booking reference embedded in the organizer confirmation link. + status: The action to take on the pending booking. + cancellation_reason: The reason the booking is being cancelled. + """ + + salt: str + status: ConfirmBookingStatus + cancellation_reason: Optional[str] = None + + +@dataclass_json +@dataclass +class DeleteBookingRequest: + """ + Class representation of a delete booking request. + + Attributes: + cancellation_reason: The reason the booking is being cancelled. + """ + + cancellation_reason: Optional[str] = None + + +@dataclass_json +@dataclass +class RescheduleBookingRequest: + """ + Class representation of a reschedule booking request. + + Attributes: + start_time: The event's start time, in Unix epoch format. + end_time: The event's end time, in Unix epoch format. + """ + + start_time: int + end_time: int + + +@dataclass_json +@dataclass +class CreateBookingQueryParams: + """ + Class representation of query parameters for creating a booking. + + Attributes: + configuration_id: The ID of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true), configuration_id is not required. + slug: The slug of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true) or using configurationId, + slug is not required. + timezone: The timezone to use for the booking. + If not provided, Nylas uses the timezone from the Configuration object. + """ + + configuration_id: Optional[str] = None + slug: Optional[str] = None + timezone: Optional[str] = None + + +class FindBookingQueryParams: + """ + Class representation of query parameters for finding a booking. + + Attributes: + configuration_id: The ID of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true), configuration_id is not required. + slug: The slug of the Configuration object whose settings are used for calculating availability. + If you're using session authentication (requires_session_auth is set to true) + or using configurationId, slug is not required. + client_id: The client ID that was used to create the Configuration object. + client_id is required only if using slug. + """ + + configuration_id: Optional[str] = None + slug: Optional[str] = None + client_id: Optional[str] = None + + +ConfirmBookingQueryParams = FindBookingQueryParams +RescheduleBookingQueryParams = FindBookingQueryParams +DestroyBookingQueryParams = FindBookingQueryParams diff --git a/nylas/resources/bookings.py b/nylas/resources/bookings.py new file mode 100644 index 00000000..0fa3cf55 --- /dev/null +++ b/nylas/resources/bookings.py @@ -0,0 +1,176 @@ +from nylas.config import RequestOverrides +from nylas.handler.api_resources import ( + CreatableApiResource, + DestroyableApiResource, + FindableApiResource, + ListableApiResource, + UpdatableApiResource, + UpdatablePatchApiResource, +) +from nylas.models.response import DeleteResponse, Response +from nylas.models.scheduler import ( + Booking, + ConfirmBookingQueryParams, + ConfirmBookingRequest, + CreateBookingQueryParams, + CreateBookingRequest, + DeleteBookingRequest, + DestroyBookingQueryParams, + RescheduleBookingRequest, + FindBookingQueryParams, + RescheduleBookingQueryParams, +) + + +class Bookings( + ListableApiResource, + FindableApiResource, + CreatableApiResource, + UpdatableApiResource, + UpdatablePatchApiResource, + DestroyableApiResource, +): + """ + Nylas Bookings API + + The Nylas Bookings API allows you to create new bookings or manage existing ones, as well as getting + bookings details for a user. + + A booking can be accessed by one, or several people, and can contain events. + """ + + def find( + self, + booking_id: str, + query_params: FindBookingQueryParams = None, + overrides: RequestOverrides = None, + ) -> Response[Booking]: + """ + Return a Booking. + + Args: + identifier: The identifier of the Grant to act upon. + booking_id: The identifier of the Booking to get. + query_params: The query parameters to include in the request. + overrides: The request overrides to use for the request. + + Returns: + The Booking. + """ + + return super().find( + path=f"/v3/scheduling/bookings/{booking_id}", + query_params=query_params, + response_type=Booking, + overrides=overrides, + ) + + def create( + self, + request_body: CreateBookingRequest, + query_params: CreateBookingQueryParams = None, + overrides: RequestOverrides = None, + ) -> Response[Booking]: + """ + Create a Booking. + + Args: + request_body: The values to create booking with. + overrides: The request overrides to use for the request. + query_params: The query parameters to include in the request. + overrides: The request overrides to use for the request. + + Returns: + The created Booking. + """ + + return super().create( + path="/v3/scheduling/bookings", + request_body=request_body, + query_params=query_params, + response_type=Booking, + overrides=overrides, + ) + + def confirm( + self, + booking_id: str, + request_body: ConfirmBookingRequest, + query_params: ConfirmBookingQueryParams = None, + overrides: RequestOverrides = None, + ) -> Response[Booking]: + """ + Confirm a Booking. + + Args: + booking_id: The identifier of the Booking to confirm. + request_body: The values to confirm booking with. + query_params: The query parameters to include in the request. + overrides: The request overrides to use for the request. + + Returns: + The confirmed Booking. + """ + + return super().update( + path=f"/v3/scheduling/bookings/{booking_id}", + request_body=request_body, + query_params=query_params, + response_type=Booking, + overrides=overrides, + ) + + def reschedule( + self, + booking_id: str, + request_body: RescheduleBookingRequest, + query_params: RescheduleBookingQueryParams = None, + overrides: RequestOverrides = None, + ) -> Response[Booking]: + """ + Reschedule a Booking. + + Args: + booking_id: The identifier of the Booking to reschedule. + request_body: The values to reschedule booking with. + query_params: The query parameters to include in the request. + overrides: The request overrides to use for the request. + + Returns: + The rescheduled Booking. + """ + + return super().patch( + path=f"/v3/scheduling/bookings/{booking_id}", + request_body=request_body, + query_params=query_params, + response_type=Booking, + overrides=overrides, + ) + + def destroy( + self, + booking_id: str, + request_body: DeleteBookingRequest, + query_params: DestroyBookingQueryParams = None, + overrides: RequestOverrides = None, + ) -> DeleteResponse: + """ + Delete a Booking. + + Args: + booking_id: The identifier of the Booking to delete. + request_body: The reason to delete booking with. + query_params: The query parameters to include in the request. + overrides: The request overrides to use for the request. + + Returns: + None. + """ + + return super().destroy( + path=f"/v3/scheduling/bookings/{booking_id}", + request_body=request_body, + query_params=query_params, + overrides=overrides, + ) diff --git a/nylas/resources/configurations.py b/nylas/resources/configurations.py new file mode 100644 index 00000000..85c47ec6 --- /dev/null +++ b/nylas/resources/configurations.py @@ -0,0 +1,160 @@ +from nylas.config import RequestOverrides +from nylas.handler.api_resources import ( + CreatableApiResource, + DestroyableApiResource, + FindableApiResource, + ListableApiResource, + UpdatableApiResource, +) +from nylas.models.list_query_params import ListQueryParams +from nylas.models.response import DeleteResponse, ListResponse, Response +from nylas.models.scheduler import ( + Configuration, + CreateConfigurationRequest, + UpdateConfigurationRequest, +) + + +class ListConfigurationsParams(ListQueryParams): + """ + Interface of the query parameters for listing configurations. + + Attributes: + limit: The maximum number of objects to return. + This field defaults to 50. The maximum allowed value is 200. + page_token: An identifier that specifies which page of data to return. + This value should be taken from a ListResponse object's next_cursor parameter. + identifier: The identifier of the Grant to act upon. + """ + + identifier: str + + +class Configurations( + ListableApiResource, + FindableApiResource, + CreatableApiResource, + UpdatableApiResource, + DestroyableApiResource, +): + """ + Nylas Configuration API + + The Nylas configuration API allows you to create new configurations or manage existing ones, as well as getting + configurations details for a user. + + Nylas Scheduler stores Configuration objects in the Scheduler database and loads + them as Scheduling Pages in the Scheduler UI. + """ + + def list( + self, + identifier: str, + query_params: ListConfigurationsParams = None, + overrides: RequestOverrides = None, + ) -> ListResponse[Configuration]: + """ + Return all Configurations. + + Args: + identifier: The identifier of the Grant to act upon. + overrides: The request overrides to use for the request. + + Returns: + The list of Configurations. + """ + # import pdb; pdb.set_trace(); + res = super().list( + path=f"/v3/grants/{identifier}/scheduling/configurations", + overrides=overrides, + response_type=Configuration, + query_params=query_params, + ) + print("What's this", res) + return res + + def find( + self, identifier: str, config_id: str, overrides: RequestOverrides = None + ) -> Response[Configuration]: + """ + Return a Configuration. + + Args: + identifier: The identifier of the Grant to act upon. + config_id: The identifier of the Configuration to get. + overrides: The request overrides to use for the request. + + Returns: + The Configuration object. + """ + return super().find( + path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}", + overrides=overrides, + response_type=Configuration, + ) + + def create( + self, + identifier: str, + request_body: CreateConfigurationRequest, + overrides: RequestOverrides = None, + ) -> Response[Configuration]: + """ + Create a new Configuration. + + Args: + identifier: The identifier of the Grant to act upon. + data: The data to create the Configuration with. + overrides: The request overrides to use for the request. + + Returns: + The Configuration object. + """ + return super().create( + path=f"/v3/grants/{identifier}/scheduling/configurations", + request_body=request_body, + overrides=overrides, + response_type=Configuration, + ) + + def update( + self, + identifier: str, + config_id: str, + request_body: UpdateConfigurationRequest, + overrides: RequestOverrides = None, + ) -> Response[Configuration]: + """ + Update a Configuration. + + Args: + identifier: The identifier of the Grant to act upon. + config_id: The identifier of the Configuration to update. + data: The data to update the Configuration with. + overrides: The request overrides to use for the request. + + Returns: + The Configuration object. + """ + return super().update( + path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}", + request_body=request_body, + overrides=overrides, + response_type=Configuration, + ) + + def destroy( + self, identifier: str, config_id: str, overrides: RequestOverrides = None + ) -> DeleteResponse: + """ + Delete a Configuration. + + Args: + identifier: The identifier of the Grant to act upon. + config_id: The identifier of the Configuration to delete. + overrides: The request overrides to use for the request. + """ + return super().destroy( + path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}", + overrides=overrides, + ) diff --git a/nylas/resources/scheduler.py b/nylas/resources/scheduler.py new file mode 100644 index 00000000..e337de46 --- /dev/null +++ b/nylas/resources/scheduler.py @@ -0,0 +1,42 @@ +from nylas.resources.bookings import Bookings +from nylas.resources.configurations import Configurations +from nylas.resources.sessions import Sessions + + +class Scheduler: + """ + Class representation of a Nylas Scheduler API. + """ + + def __init__(self, http_client): + self.http_client = http_client + + @property + def configurations(self) -> Configurations: + """ + Access the Configurations API. + + Returns: + The Configurations API. + """ + return Configurations(self.http_client) + + @property + def bookings(self) -> Bookings: + """ + Access the Bookings API. + + Returns: + The Bookings API. + """ + return Bookings(self.http_client) + + @property + def sessions(self) -> Sessions: + """ + Access the Sessions API. + + Returns: + The Sessions API. + """ + return Sessions(self.http_client) diff --git a/nylas/resources/sessions.py b/nylas/resources/sessions.py new file mode 100644 index 00000000..556009a4 --- /dev/null +++ b/nylas/resources/sessions.py @@ -0,0 +1,56 @@ +from nylas.config import RequestOverrides +from nylas.handler.api_resources import CreatableApiResource, DestroyableApiResource +from nylas.models.response import DeleteResponse, Response +from nylas.models.scheduler import CreateSessionRequest, Session + + +class Sessions(CreatableApiResource, DestroyableApiResource): + """ + Nylas Sessions API + + The Nylas Sessions API allows you to create new sessions or manage existing ones. + """ + + def create( + self, + request_body: CreateSessionRequest, + overrides: RequestOverrides = None, + ) -> Response[Session]: + """ + Create a Session. + + Args: + request_body: The request body to create the Session. + overrides: The request overrides to use for the request. + + Returns: + The Session. + """ + + return super().create( + path="/v3/scheduling/sessions", + request_body=request_body, + response_type=Session, + overrides=overrides, + ) + + def destroy( + self, + session_id: str, + overrides: RequestOverrides = None, + ) -> DeleteResponse: + """ + Destroy a Session. + + Args: + session_id: The identifier of the Session to destroy. + overrides: The request overrides to use for the request. + + Returns: + None. + """ + + return super().destroy( + path=f"/v3/scheduling/sessions/{session_id}", + overrides=overrides, + ) diff --git a/tests/resources/test_bookings.py b/tests/resources/test_bookings.py new file mode 100644 index 00000000..15dc87e2 --- /dev/null +++ b/tests/resources/test_bookings.py @@ -0,0 +1,116 @@ +from nylas.resources.bookings import Bookings + +from nylas.models.scheduler import Booking + +class TestBooking: + def test_booking_deserialization(self): + booking_json = { + "booking_id": "AAAA-BBBB-1111-2222", + "event_id": "CCCC-DDDD-3333-4444", + "title": "My test event", + "organizer": { + "name": "John Doe", + "email": "user@example.com" + }, + "status": "booked", + "description": "This is an example of a description." + } + + booking = Booking.from_dict(booking_json) + + assert booking.booking_id == "AAAA-BBBB-1111-2222" + assert booking.event_id == "CCCC-DDDD-3333-4444" + assert booking.title == "My test event" + assert booking.organizer.name == "John Doe" + assert booking.organizer.email == "user@example.com" + assert booking.status == "booked" + assert booking.description == "This is an example of a description." + + def test_find_booking(self, http_client_response): + bookings = Bookings(http_client_response) + + bookings.find(booking_id="booking-123") + + http_client_response._execute.assert_called_once_with( + "GET", + "/v3/scheduling/bookings/booking-123", + None, + None, + None, + overrides=None + ) + + def test_create_booking(self, http_client_response): + bookings = Bookings(http_client_response) + request_body = { + "start_time": 1730725200, + "end_time": 1730727000, + "participants": [ + { + "email": "test@nylas.com" + } + ], + "guest": { + "name": "TEST", + "email": "user@gmail.com" + } + } + bookings.create(request_body=request_body) + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/scheduling/bookings", + None, + None, + request_body, + overrides=None + ) + + def test_confirm_booking(self, http_client_response): + bookings = Bookings(http_client_response) + request_body = { + "salt": "_zfg12it", + "status": "cancelled", + } + + bookings.confirm(booking_id="booking-123", request_body=request_body) + http_client_response._execute.assert_called_once_with( + "PUT", + "/v3/scheduling/bookings/booking-123", + None, + None, + request_body, + overrides=None + ) + + def test_reschedule_booking(self, http_client_response): + bookings = Bookings(http_client_response) + request_body = { + "start_time": 1730725200, + "end_time": 1730727000, + } + + bookings.reschedule(booking_id="booking-123", request_body=request_body) + http_client_response._execute.assert_called_once_with( + "PATCH", + "/v3/scheduling/bookings/booking-123", + None, + None, + request_body, + overrides=None + ) + + def test_destroy_booking(self, http_client_delete_response): + bookings = Bookings(http_client_delete_response) + request_body = { + "cancellation_reason": "I am no longer available at this time." + } + bookings.destroy(booking_id="booking-123", request_body=request_body) + + http_client_delete_response._execute.assert_called_once_with( + "DELETE", + "/v3/scheduling/bookings/booking-123", + None, + None, + request_body, + overrides=None + ) \ No newline at end of file diff --git a/tests/resources/test_calendars.py b/tests/resources/test_calendars.py index 2f0aae9d..7fe6b1fd 100644 --- a/tests/resources/test_calendars.py +++ b/tests/resources/test_calendars.py @@ -167,7 +167,7 @@ def test_get_availability(self, http_client_response): "exdates": ["2021-03-01"], } ], - "round_robin_event_id": "event-123", + "round_robin_group_id": "event-123", }, } diff --git a/tests/resources/test_configurations.py b/tests/resources/test_configurations.py new file mode 100644 index 00000000..8b2388f3 --- /dev/null +++ b/tests/resources/test_configurations.py @@ -0,0 +1,234 @@ +from nylas.resources.configurations import Configurations + +from nylas.models.scheduler import Configuration + +class TestConfiguration: + def test_configuration_deserialization(self): + configuration_json = { + "id": "abc-123-configuration-id", + "slug": None, + "participants": [ + { + "email": "test@nylas.com", + "is_organizer": True, + "name": "Test", + "availability": { + "calendar_ids": [ + "primary" + ], + "open_hours": [ + { + "days": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "exdates": None, + "timezone": "", + "start": "09:00", + "end": "17:00" + } + ] + }, + "booking": { + "calendar_id": "primary" + }, + "timezone": "" + } + ], + "requires_session_auth": False, + "availability": { + "duration_minutes": 30, + "interval_minutes": 15, + "round_to": 15, + "availability_rules": { + "availability_method": "collective", + "buffer": { + "before": 60, + "after": 0 + }, + "default_open_hours": [ + { + "days": [ + 0, + 1, + 2, + 5, + 6 + ], + "exdates": None, + "timezone": "", + "start": "09:00", + "end": "18:00" + } + ], + "round_robin_group_id": "" + } + }, + "event_booking": { + "title": "Updated Title", + "timezone": "utc", + "description": "", + "location": "none", + "booking_type": "booking", + "conferencing": { + "provider": "Microsoft Teams", + "autocreate": { + "conf_grant_id": "", + "conf_settings": None + } + }, + "hide_participants": None, + "disable_emails": None + }, + "scheduler": { + "available_days_in_future": 7, + "min_cancellation_notice": 60, + "min_booking_notice": 120, + "confirmation_redirect_url": "", + "hide_rescheduling_options": False, + "hide_cancellation_options": False, + "hide_additional_guests": True, + "cancellation_policy": "", + "email_template": { + "booking_confirmed": {} + } + }, + "appearance": { + "submit_button_label": "submit", + "thank_you_message": "thank you for your business. your booking was successful." + } + } + + configuration = Configuration.from_dict(configuration_json) + + assert configuration.id == "abc-123-configuration-id" + assert configuration.slug == None + assert configuration.participants[0].email == "test@nylas.com" + assert configuration.participants[0].is_organizer == True + assert configuration.participants[0].name == "Test" + assert configuration.participants[0].availability.calendar_ids == ["primary"] + assert configuration.participants[0].availability.open_hours[0]["days"] == [0, 1, 2, 3, 4, 5, 6] + assert configuration.participants[0].availability.open_hours[0]["exdates"] == None + assert configuration.participants[0].availability.open_hours[0]["timezone"] == "" + assert configuration.participants[0].booking.calendar_id == "primary" + assert configuration.participants[0].timezone == "" + assert configuration.requires_session_auth == False + assert configuration.availability.duration_minutes == 30 + assert configuration.availability.interval_minutes == 15 + assert configuration.availability.round_to == 15 + assert configuration.availability.availability_rules["availability_method"] == "collective" + assert configuration.availability.availability_rules["buffer"]["before"] == 60 + assert configuration.availability.availability_rules["buffer"]["after"] == 0 + assert configuration.availability.availability_rules["default_open_hours"][0]["days"] == [0, 1, 2, 5, 6] + assert configuration.availability.availability_rules["default_open_hours"][0]["exdates"] == None + assert configuration.availability.availability_rules["default_open_hours"][0]["timezone"] == "" + assert configuration.availability.availability_rules["default_open_hours"][0]["start"] == "09:00" + assert configuration.availability.availability_rules["default_open_hours"][0]["end"] == "18:00" + assert configuration.event_booking.title == "Updated Title" + assert configuration.event_booking.timezone == "utc" + assert configuration.event_booking.description == "" + assert configuration.event_booking.location == "none" + assert configuration.event_booking.booking_type == "booking" + assert configuration.event_booking.conferencing.provider == "Microsoft Teams" + assert configuration.scheduler.available_days_in_future == 7 + assert configuration.scheduler.min_cancellation_notice == 60 + assert configuration.scheduler.min_booking_notice == 120 + assert configuration.appearance["submit_button_label"] == "submit" + + def test_list_configurations(self, http_client_list_response): + configurations = Configurations(http_client_list_response) + configurations.list(identifier="grant-123") + + http_client_list_response._execute.assert_called_once_with( + "GET", + "/v3/grants/grant-123/scheduling/configurations", + None, + None, + None, + overrides=None, + ) + + def test_find_configuration(self, http_client_response): + configurations = Configurations(http_client_response) + configurations.find(identifier="grant-123", config_id="config-123") + + http_client_response._execute.assert_called_once_with( + "GET", + "/v3/grants/grant-123/scheduling/configurations/config-123", + None, + None, + None, + overrides=None, + ) + + def test_create_configuration(self, http_client_response): + configurations = Configurations(http_client_response) + request_body = { + "requires_session_auth": False, + "participants": [ + { + "name": "Test", + "email": "test@nylas.com", + "is_organizer": True, + "availability": { + "calendar_ids": [ + "primary" + ] + }, + "booking": { + "calendar_id": "primary" + } + } + ], + "availability": { + "duration_minutes": 30 + }, + "event_booking": { + "title": "My test event" + } + } + configurations.create(identifier="grant-123", request_body=request_body) + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/grants/grant-123/scheduling/configurations", + None, + None, + request_body, + overrides=None, + ) + + def test_update_configuration(self, http_client_response): + configurations = Configurations(http_client_response) + request_body = { + "event_booking": { + "title": "My test event" + } + } + configurations.update(identifier="grant-123", config_id="config-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + "PUT", + "/v3/grants/grant-123/scheduling/configurations/config-123", + None, + None, + request_body, + overrides=None, + ) + + def test_destroy_configuration(self, http_client_delete_response): + configurations = Configurations(http_client_delete_response) + configurations.destroy(identifier="grant-123", config_id="config-123") + + http_client_delete_response._execute.assert_called_once_with( + "DELETE", + "/v3/grants/grant-123/scheduling/configurations/config-123", + None, + None, + None, + overrides=None, + ) \ No newline at end of file diff --git a/tests/resources/test_sessions.py b/tests/resources/test_sessions.py new file mode 100644 index 00000000..5d4590db --- /dev/null +++ b/tests/resources/test_sessions.py @@ -0,0 +1,45 @@ +from nylas.resources.scheduler import Sessions + +from nylas.models.scheduler import Session + +class TestSession: + def test_session_deserialization(self): + session_json = { + "session_id": "session-id", + } + + session = Session.from_dict(session_json) + + assert session.session_id == "session-id" + + def test_create_session(self, http_client_response): + sessions = Sessions(http_client_response) + request_body = { + "configuration_id": "configuration-123", + "time_to_live": 30 + } + + sessions.create(request_body) + + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/scheduling/sessions", + None, + None, + request_body, + overrides=None, + ) + + def test_destroy_session(self, http_client_delete_response): + sessions = Sessions(http_client_delete_response) + + sessions.destroy(session_id="session-123") + + http_client_delete_response._execute.assert_called_once_with( + "DELETE", + "/v3/scheduling/sessions/session-123", + None, + None, + None, + overrides=None, + ) \ No newline at end of file From b68c89331be14dda0e1fe4d169eeb4c70004ec17 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:56:52 -0500 Subject: [PATCH 2/2] fix: handle missing attributes when using select param (#397) --- nylas/models/response.py | 2 +- tests/resources/test_threads.py | 44 ++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/nylas/models/response.py b/nylas/models/response.py index d00798c5..f8a91652 100644 --- a/nylas/models/response.py +++ b/nylas/models/response.py @@ -94,7 +94,7 @@ def from_dict(cls, resp: dict, generic_type): converted_data = [] for item in resp["data"]: - converted_data.append(generic_type.from_dict(item)) + converted_data.append(generic_type.from_dict(item, infer_missing=True)) return cls( data=converted_data, diff --git a/tests/resources/test_threads.py b/tests/resources/test_threads.py index 47c9cbec..749c4b90 100644 --- a/tests/resources/test_threads.py +++ b/tests/resources/test_threads.py @@ -1,7 +1,7 @@ from nylas.models.attachments import Attachment from nylas.models.events import EmailName +from nylas.models.response import ListResponse from nylas.resources.threads import Threads - from nylas.models.threads import Thread @@ -147,6 +147,48 @@ def test_list_threads_with_query_params(self, http_client_list_response): overrides=None, ) + def test_list_threads_with_select_param(self, http_client_list_response): + threads = Threads(http_client_list_response) + + # Set up mock response data + http_client_list_response._execute.return_value = { + "request_id": "abc-123", + "data": [{ + "id": "thread-123", + "has_attachments": False, + "earliest_message_date": 1634149514, + "participants": [ + {"email": "test@example.com", "name": "Test User"} + ], + "snippet": "Test snippet", + "unread": False, + "subject": "Test subject", + "message_ids": ["msg-123"], + "folders": ["folder-123"] + }] + } + + # Call the API method + result = threads.list( + identifier="abc-123", + query_params={ + "select": "id,has_attachments,earliest_message_date,participants,snippet,unread,subject,message_ids,folders" + } + ) + + # Verify API call + http_client_list_response._execute.assert_called_with( + "GET", + "/v3/grants/abc-123/threads", + None, + {"select": "id,has_attachments,earliest_message_date,participants,snippet,unread,subject,message_ids,folders"}, + None, + overrides=None, + ) + + # The actual response validation is handled by the mock in conftest.py + assert result is not None + def test_find_thread(self, http_client_response): threads = Threads(http_client_response)