reflex-schedule-x 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ """Reflex custom component for the Schedule-X event calendar (https://schedule-x.dev)."""
2
+
3
+ from .helpers import (
4
+ LOCALES,
5
+ ScheduleXAPI,
6
+ background_event,
7
+ calendar_event,
8
+ calendar_type,
9
+ parse_sx_datetime,
10
+ to_sx_date,
11
+ to_sx_datetime,
12
+ )
13
+ from .schedule_x import (
14
+ LIB_DEPENDENCIES,
15
+ SCHEDULE_X_VERSION,
16
+ SLOT_NAMES,
17
+ VIEWS,
18
+ CalendarEvent,
19
+ DateRange,
20
+ RenderInfo,
21
+ ScheduleX,
22
+ ScheduleXAction,
23
+ ScheduleXDatePicker,
24
+ ScheduleXEventCard,
25
+ ScheduleXField,
26
+ ScheduleXShow,
27
+ ScheduleXSlot,
28
+ ScheduleXTimePicker,
29
+ SlotAction,
30
+ schedule_x,
31
+ schedule_x_action,
32
+ schedule_x_date_picker,
33
+ schedule_x_event_card,
34
+ schedule_x_field,
35
+ schedule_x_show,
36
+ schedule_x_slot,
37
+ schedule_x_time_picker,
38
+ )
39
+
40
+ __all__ = [
41
+ "LIB_DEPENDENCIES",
42
+ "LOCALES",
43
+ "SCHEDULE_X_VERSION",
44
+ "SLOT_NAMES",
45
+ "VIEWS",
46
+ "CalendarEvent",
47
+ "DateRange",
48
+ "RenderInfo",
49
+ "ScheduleX",
50
+ "ScheduleXAPI",
51
+ "ScheduleXAction",
52
+ "ScheduleXDatePicker",
53
+ "ScheduleXEventCard",
54
+ "ScheduleXField",
55
+ "ScheduleXShow",
56
+ "ScheduleXSlot",
57
+ "ScheduleXTimePicker",
58
+ "SlotAction",
59
+ "background_event",
60
+ "calendar_event",
61
+ "calendar_type",
62
+ "parse_sx_datetime",
63
+ "schedule_x",
64
+ "schedule_x_action",
65
+ "schedule_x_date_picker",
66
+ "schedule_x_event_card",
67
+ "schedule_x_field",
68
+ "schedule_x_show",
69
+ "schedule_x_slot",
70
+ "schedule_x_time_picker",
71
+ "to_sx_date",
72
+ "to_sx_datetime",
73
+ ]
@@ -0,0 +1,332 @@
1
+ """Python helpers for building Schedule-X data and driving a calendar imperatively."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as _dt
6
+ import json
7
+ from typing import Any
8
+
9
+ import reflex as rx
10
+
11
+ LOCALES: tuple[str, ...] = (
12
+ "ar-EG", "ca-ES", "cs-CZ", "da-DK", "de-DE", "en-GB", "en-US", "es-ES", "et-EE",
13
+ "fa-IR", "fi-FI", "fr-CH", "fr-FR", "he-IL", "hr-HR", "id-ID", "it-IT", "ja-JP",
14
+ "ko-KR", "ky-KG", "lt-LT", "mk-MK", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "ro-RO",
15
+ "ru-RU", "sk-SK", "sl-SI", "sr-Latn-RS", "sr-RS", "sv-SE", "tr-TR", "uk-UA",
16
+ "zh-CN", "zh-TW",
17
+ ) # fmt: skip
18
+ """Locales shipped with @schedule-x/translations."""
19
+
20
+ DateLike = str | _dt.date | _dt.datetime
21
+
22
+
23
+ # --------------------------------------------------------------------------- #
24
+ # Date conversion #
25
+ # --------------------------------------------------------------------------- #
26
+
27
+
28
+ def to_sx_date(value: DateLike) -> str:
29
+ """Convert a date (or datetime) to the 'YYYY-MM-DD' string the calendar expects.
30
+
31
+ Args:
32
+ value: A ``date``, ``datetime`` or string.
33
+
34
+ Returns:
35
+ The date string.
36
+ """
37
+ if isinstance(value, _dt.datetime):
38
+ return value.date().isoformat()
39
+ if isinstance(value, _dt.date):
40
+ return value.isoformat()
41
+ return str(value)[:10]
42
+
43
+
44
+ def to_sx_datetime(value: DateLike) -> str:
45
+ """Convert a Python value to a Schedule-X start/end string.
46
+
47
+ - ``date`` -> 'YYYY-MM-DD' (all-day)
48
+ - naive ``datetime`` -> 'YYYY-MM-DD HH:mm' (interpreted in the calendar timezone)
49
+ - aware ``datetime`` -> ISO 8601 with offset (converted to the calendar timezone)
50
+
51
+ Args:
52
+ value: A ``date``, ``datetime`` or already formatted string.
53
+
54
+ Returns:
55
+ The string for ``start``/``end``.
56
+ """
57
+ if isinstance(value, _dt.datetime):
58
+ if value.tzinfo is not None and value.utcoffset() is not None:
59
+ return value.isoformat(timespec="seconds")
60
+ return value.strftime("%Y-%m-%d %H:%M")
61
+ if isinstance(value, _dt.date):
62
+ return value.isoformat()
63
+ return str(value)
64
+
65
+
66
+ def parse_sx_datetime(value: str) -> _dt.date | _dt.datetime:
67
+ """Parse a date/date-time string sent by the calendar back into Python.
68
+
69
+ Accepts 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm' and RFC 9557 strings
70
+ ('2025-01-01T10:00:00-04:00[America/Caracas]').
71
+
72
+ Args:
73
+ value: The string from an event payload.
74
+
75
+ Returns:
76
+ A ``date`` for all-day values, otherwise a ``datetime``.
77
+ """
78
+ text = value.strip()
79
+ if len(text) == 10:
80
+ return _dt.date.fromisoformat(text)
81
+ if "[" in text:
82
+ text = text.split("[", 1)[0]
83
+ return _dt.datetime.fromisoformat(text.replace(" ", "T"))
84
+
85
+
86
+ # --------------------------------------------------------------------------- #
87
+ # Data builders #
88
+ # --------------------------------------------------------------------------- #
89
+
90
+
91
+ def calendar_event(
92
+ id: str | int,
93
+ start: DateLike,
94
+ end: DateLike | None = None,
95
+ title: str | None = None,
96
+ *,
97
+ description: str | None = None,
98
+ location: str | None = None,
99
+ people: list[str] | None = None,
100
+ calendar_id: str | None = None,
101
+ rrule: str | None = None,
102
+ exdate: list[str] | None = None,
103
+ disable_dnd: bool | None = None,
104
+ disable_resize: bool | None = None,
105
+ additional_classes: list[str] | None = None,
106
+ custom_content: dict[str, str] | None = None,
107
+ **extra: Any,
108
+ ) -> dict[str, Any]:
109
+ """Build an event dict for ``schedule_x(events=[...])``.
110
+
111
+ Args:
112
+ id: Unique id.
113
+ start: Start (date for all-day, datetime for timed events).
114
+ end: End; defaults to ``start``.
115
+ title: Title.
116
+ description: Description shown in the event modal.
117
+ location: Location.
118
+ people: Participants.
119
+ calendar_id: Key of a calendar in ``calendars``.
120
+ rrule: RFC 5545 recurrence rule (needs ``recurrence=True``).
121
+ exdate: Excluded dates, e.g. ['20250101T100000'].
122
+ disable_dnd: Disable drag & drop for this event (premium plugin).
123
+ disable_resize: Disable resizing for this event (premium plugin).
124
+ additional_classes: Extra CSS classes for the event element.
125
+ custom_content: HTML per view: timeGrid, dateGrid, monthGrid, monthAgenda, weekAgenda.
126
+ **extra: Any other keys; they are returned untouched in callbacks.
127
+
128
+ Returns:
129
+ The event dict.
130
+ """
131
+ event: dict[str, Any] = {
132
+ "id": id,
133
+ "start": to_sx_datetime(start),
134
+ "end": to_sx_datetime(end if end is not None else start),
135
+ }
136
+ optional = {
137
+ "title": title,
138
+ "description": description,
139
+ "location": location,
140
+ "people": people,
141
+ "calendarId": calendar_id,
142
+ "rrule": rrule,
143
+ "exdate": exdate,
144
+ }
145
+ event.update({k: v for k, v in optional.items() if v is not None})
146
+ options = {
147
+ "disableDND": disable_dnd,
148
+ "disableResize": disable_resize,
149
+ "additionalClasses": additional_classes,
150
+ }
151
+ options = {k: v for k, v in options.items() if v is not None}
152
+ if options:
153
+ event["_options"] = options
154
+ if custom_content:
155
+ event["_customContent"] = custom_content
156
+ event.update(extra)
157
+ return event
158
+
159
+
160
+ def background_event(
161
+ start: DateLike,
162
+ end: DateLike | None = None,
163
+ style: dict[str, Any] | None = None,
164
+ title: str | None = None,
165
+ *,
166
+ rrule: str | None = None,
167
+ exdate: list[str] | None = None,
168
+ ) -> dict[str, Any]:
169
+ """Build a background event (holidays, out-of-office blocks, ...).
170
+
171
+ Args:
172
+ start: Start.
173
+ end: End; defaults to ``start``.
174
+ style: CSS for the block (camelCase or snake_case keys).
175
+ title: Tooltip.
176
+ rrule: Recurrence rule (needs ``recurrence=True``).
177
+ exdate: Excluded dates.
178
+
179
+ Returns:
180
+ The background event dict.
181
+ """
182
+ event: dict[str, Any] = {
183
+ "start": to_sx_datetime(start),
184
+ "end": to_sx_datetime(end if end is not None else start),
185
+ "style": style or {"backgroundColor": "rgba(128, 128, 128, 0.15)"},
186
+ }
187
+ if title is not None:
188
+ event["title"] = title
189
+ if rrule is not None:
190
+ event["rrule"] = rrule
191
+ if exdate is not None:
192
+ event["exdate"] = exdate
193
+ return event
194
+
195
+
196
+ def calendar_type(
197
+ color_name: str,
198
+ main: str,
199
+ container: str,
200
+ on_container: str,
201
+ *,
202
+ dark_main: str | None = None,
203
+ dark_container: str | None = None,
204
+ dark_on_container: str | None = None,
205
+ label: str | None = None,
206
+ readonly: bool | None = None,
207
+ ) -> dict[str, Any]:
208
+ """Build a calendar category for ``schedule_x(calendars={...})``.
209
+
210
+ Args:
211
+ color_name: Lower-case name used in CSS variables (``--sx-color-<name>``).
212
+ main: Main color (light mode).
213
+ container: Background color (light mode).
214
+ on_container: Text color on the background (light mode).
215
+ dark_main: Main color in dark mode (defaults to ``container``).
216
+ dark_container: Background in dark mode (defaults to ``main``).
217
+ dark_on_container: Text color in dark mode (defaults to ``container``).
218
+ label: Human-readable name.
219
+ readonly: Mark events of this calendar as read-only (premium interactions).
220
+
221
+ Returns:
222
+ The calendar dict.
223
+ """
224
+ cal: dict[str, Any] = {
225
+ "colorName": color_name,
226
+ "lightColors": {"main": main, "container": container, "onContainer": on_container},
227
+ "darkColors": {
228
+ "main": dark_main or container,
229
+ "container": dark_container or main,
230
+ "onContainer": dark_on_container or container,
231
+ },
232
+ }
233
+ if label is not None:
234
+ cal["label"] = label
235
+ if readonly is not None:
236
+ cal["readonly"] = readonly
237
+ return cal
238
+
239
+
240
+ # --------------------------------------------------------------------------- #
241
+ # Imperative API #
242
+ # --------------------------------------------------------------------------- #
243
+
244
+
245
+ class ScheduleXAPI:
246
+ """Drive a calendar rendered with ``schedule_x(id=...)`` from event handlers.
247
+
248
+ Every method returns an ``rx.call_script`` event, so return it (or yield it)
249
+ from an event handler, or use it directly as an ``on_click``.
250
+
251
+ Example:
252
+ ```python
253
+ api = ScheduleXAPI("my-calendar")
254
+ rx.button("Week", on_click=api.set_view("week"))
255
+
256
+ @rx.event
257
+ def refresh(self):
258
+ return api.get_events(callback=State.receive_events)
259
+ ```
260
+ """
261
+
262
+ def __init__(self, calendar_id: str):
263
+ """Bind the API to a calendar id.
264
+
265
+ Args:
266
+ calendar_id: The ``id`` passed to ``schedule_x``.
267
+ """
268
+ self.calendar_id = calendar_id
269
+
270
+ def _call(self, method: str, *args: Any, callback: Any = None) -> Any:
271
+ arguments = ", ".join(json.dumps(a, default=str) for a in args)
272
+ target = f"window.__reflexScheduleX?.[{json.dumps(self.calendar_id)}]"
273
+ script = f"(() => {{ const api = {target}; return api ? api.{method}({arguments}) : null; }})()"
274
+ if callback is None:
275
+ return rx.call_script(script)
276
+ return rx.call_script(script, callback=callback)
277
+
278
+ def set_view(self, view: str) -> Any:
279
+ """Change the view ('day', 'week', 'month-grid', ...)."""
280
+ return self._call("setView", view)
281
+
282
+ def set_date(self, date: DateLike) -> Any:
283
+ """Navigate to a date."""
284
+ return self._call("setDate", to_sx_date(date))
285
+
286
+ def set_theme(self, mode: str) -> Any:
287
+ """Switch between 'light' and 'dark'."""
288
+ return self._call("setTheme", mode)
289
+
290
+ def add_event(self, event: dict[str, Any]) -> Any:
291
+ """Add an event without going through the ``events`` prop."""
292
+ return self._call("addEvent", event)
293
+
294
+ def update_event(self, event: dict[str, Any]) -> Any:
295
+ """Update an event (matched by id)."""
296
+ return self._call("updateEvent", event)
297
+
298
+ def remove_event(self, event_id: str | int) -> Any:
299
+ """Remove an event by id."""
300
+ return self._call("removeEvent", event_id)
301
+
302
+ def set_events(self, events: list[dict[str, Any]]) -> Any:
303
+ """Replace all events."""
304
+ return self._call("setEvents", events)
305
+
306
+ def close_event_modal(self) -> Any:
307
+ """Close the event modal plugin."""
308
+ return self._call("closeEventModal")
309
+
310
+ def scroll_to(self, time: str) -> Any:
311
+ """Scroll the week/day grid to 'HH:mm' (needs ``initial_scroll``)."""
312
+ return self._call("scrollTo", time)
313
+
314
+ def get_view(self, callback: Any) -> Any:
315
+ """Send the current view name to ``callback``."""
316
+ return self._call("getView", callback=callback)
317
+
318
+ def get_date(self, callback: Any) -> Any:
319
+ """Send the selected date to ``callback``."""
320
+ return self._call("getDate", callback=callback)
321
+
322
+ def get_range(self, callback: Any) -> Any:
323
+ """Send the visible range ``{start, end}`` to ``callback``."""
324
+ return self._call("getRange", callback=callback)
325
+
326
+ def get_events(self, callback: Any) -> Any:
327
+ """Send all events currently in the calendar to ``callback``."""
328
+ return self._call("getEvents", callback=callback)
329
+
330
+ def get_event(self, event_id: str | int, callback: Any) -> Any:
331
+ """Send one event to ``callback``."""
332
+ return self._call("getEvent", event_id, callback=callback)
File without changes