victoria-latynina commited on
Commit
62bdda7
·
verified ·
1 Parent(s): 53642fb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -0
app.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from datetime import datetime, timedelta
4
+ import logging
5
+ import requests
6
+
7
+ import gradio as gr
8
+
9
+ # -------------------------------------------------------------
10
+ # Configure logging
11
+ # -------------------------------------------------------------
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # -------------------------------------------------------------
16
+ # Minimal Mindbody API wrapper
17
+ # -------------------------------------------------------------
18
+ class MindbodyClient:
19
+ """Thin wrapper around the MINDBODY Public API (v6).
20
+
21
+ Only the endpoints needed for this demo are implemented. Extend freely!
22
+ """
23
+
24
+ def __init__(self, api_key: str, site_id: str, source_name: str | None = None):
25
+ self.base_url = "https://api.mindbodyonline.com/public/v6"
26
+ self.headers = {
27
+ "API-Key": api_key,
28
+ "SiteId": site_id,
29
+ "Content-Type": "application/json",
30
+ }
31
+ if source_name:
32
+ self.headers["SourceName"] = source_name
33
+
34
+ # --------------------------- internal helpers ---------------------------
35
+ def _get(self, path: str, params: dict | None = None):
36
+ url = f"{self.base_url}{path}"
37
+ logger.debug("GET %s", url)
38
+ resp = requests.get(url, headers=self.headers, params=params, timeout=30)
39
+ resp.raise_for_status()
40
+ return resp.json()
41
+
42
+ def _post(self, path: str, payload: dict):
43
+ url = f"{self.base_url}{path}"
44
+ logger.debug("POST %s", url)
45
+ resp = requests.post(url, headers=self.headers, json=payload, timeout=30)
46
+ resp.raise_for_status()
47
+ return resp.json()
48
+
49
+ # --------------------------- public endpoints --------------------------
50
+ def get_class_descriptions(self, location_id: str | int):
51
+ """Return **types of training** offered at a location."""
52
+ params = {"LocationIds": location_id}
53
+ return self._get("/class/classdescriptions", params)
54
+
55
+ def get_classes(self, location_id: str | int, start_iso: str, end_iso: str):
56
+ """Return **scheduled classes** for a date range."""
57
+ params = {
58
+ "LocationIds": location_id,
59
+ "StartDateTime": start_iso,
60
+ "EndDateTime": end_iso,
61
+ }
62
+ return self._get("/class/classes", params)
63
+
64
+ def book_class(self, class_id: int, client_id: str, cross_regional: bool = False):
65
+ """Book a client into a class *after* verifying availability."""
66
+ # 1) confirm availability
67
+ info = self._get("/class/classes", {"ClassIds": class_id})
68
+ if not info.get("Classes"):
69
+ raise ValueError("Class ID not found")
70
+
71
+ cls = info["Classes"][0]
72
+ if not cls.get("IsAvailable", False):
73
+ raise ValueError("Class is not available for online booking")
74
+ if cls.get("TotalBooked", 0) >= cls.get("MaxCapacity", 0):
75
+ raise ValueError("Class is already full")
76
+
77
+ # 2) book
78
+ payload = {
79
+ "ClientId": client_id,
80
+ "ClassId": class_id,
81
+ "CrossRegionalBooking": cross_regional,
82
+ }
83
+ return self._post("/class/addclienttoclass", payload)
84
+
85
+
86
+ # -------------------------------------------------------------
87
+ # Global state (kept super-simple for a single-user MCP demo)
88
+ # -------------------------------------------------------------
89
+ mb_client: MindbodyClient | None = None
90
+
91
+
92
+ # -------------------------------------------------------------
93
+ # Helper functions wired to Gradio components
94
+ # -------------------------------------------------------------
95
+
96
+ def initialize_mindbody_client():
97
+ """Instantiate the global MindbodyClient reading credentials from ./config/.env"""
98
+ global mb_client
99
+ api_key = os.getenv("MINDBODY_API_KEY")
100
+ site_id = os.getenv("MINDBODY_SITE_ID")
101
+ source_name = os.getenv("MINDBODY_SOURCE_NAME") # optional
102
+
103
+ if not api_key or not site_id:
104
+ return "❌ MINDBODY_API_KEY or MINDBODY_SITE_ID missing in .env"
105
+
106
+ try:
107
+ mb_client = MindbodyClient(api_key, site_id, source_name)
108
+ return "✅ Mindbody client initialised. Ready to query!"
109
+ except Exception as exc:
110
+ logger.exception("Failed initialising Mindbody client")
111
+ return f"❌ Initialisation error: {exc}"
112
+
113
+
114
+ def get_training_types(location_id: str | int):
115
+ """Fetch human-readable list of class description names."""
116
+ if not mb_client:
117
+ return "❌ Authenticate first."
118
+
119
+ try:
120
+ res = mb_client.get_class_descriptions(location_id)
121
+ descs = res.get("ClassDescriptions", [])
122
+ if not descs:
123
+ return "⚠️ No training types found for this location."
124
+
125
+ lines = [f"- {d.get('Name')} (ID: {d.get('Id')})" for d in descs]
126
+ return "🏷️ Training Types\n" + "\n".join(lines)
127
+ except Exception as exc:
128
+ logger.exception("Error while fetching class descriptions")
129
+ return f"❌ Error: {exc}"
130
+
131
+
132
+ def get_schedule(location_id: str | int, days: int):
133
+ """Return upcoming classes in a human-friendly Markdown list."""
134
+ if not mb_client:
135
+ return "❌ Authenticate first."
136
+
137
+ try:
138
+ start_iso = datetime.now().strftime("%Y-%m-%dT00:00:00")
139
+ end_iso = (datetime.now() + timedelta(days=int(days))).strftime("%Y-%m-%dT23:59:59")
140
+ res = mb_client.get_classes(location_id, start_iso, end_iso)
141
+ classes = res.get("Classes", [])
142
+ if not classes:
143
+ return "⚠️ No classes in the selected window."
144
+
145
+ formatted = []
146
+ for c in classes:
147
+ dt = c.get("StartDateTime", "?")
148
+ name = c.get("ClassDescription", {}).get("Name", "🆔" + str(c.get("ClassDescriptionId")))
149
+ ok = "✅" if c.get("IsAvailable") else "❌"
150
+ cid = c.get("Id")
151
+ formatted.append(f"{dt} | {name} | {ok} | ClassId: {cid}")
152
+
153
+ return "📅 Schedule\n" + "\n".join(formatted)
154
+ except Exception as exc:
155
+ logger.exception("Error while fetching schedule")
156
+ return f"❌ Error: {exc}"
157
+
158
+
159
+ def book_class_gr(class_id: int, client_id: str, cross_regional: bool):
160
+ """Book a class through the UI."""
161
+ if not mb_client:
162
+ return "❌ Authenticate first."
163
+
164
+ try:
165
+ res = mb_client.book_class(class_id, client_id, cross_regional)
166
+ visit = res.get("Visit", {})
167
+ status = visit.get("AppointmentStatus", "OK")
168
+ return f"🎉 Booked! Status: {status}\n\nFull response:\n{res}"
169
+ except Exception as exc:
170
+ logger.exception("Booking failed")
171
+ return f"❌ Could not book: {exc}"
172
+
173
+
174
+ # -------------------------------------------------------------
175
+ # Gradio UI definition
176
+ # -------------------------------------------------------------
177
+ with gr.Blocks(title="Mindbody Class Booker") as demo:
178
+ gr.Markdown("# 🏋️‍♀️ MINDBODY Class Explorer & Booker")
179
+ gr.Markdown("Authenticate, browse classes by location, and secure your spot – all in one place.")
180
+
181
+ # ---------- Auth ----------
182
+ with gr.Row():
183
+ auth_out = gr.Textbox(label="Auth Status", interactive=False)
184
+ auth_btn = gr.Button("Authenticate")
185
+
186
+ # ---------- Training types ----------
187
+ with gr.Row():
188
+ loc_in = gr.Textbox(label="Location ID", placeholder="e.g. -99")
189
+ types_out = gr.Textbox(label="Training Types", lines=10, interactive=False)
190
+ types_btn = gr.Button("Get Training Types")
191
+
192
+ # ---------- Schedule ----------
193
+ with gr.Row():
194
+ days_in = gr.Number(value=7, label="Days Ahead")
195
+ sched_out = gr.Textbox(label="Class Schedule", lines=12, interactive=False)
196
+ sched_btn = gr.Button("Get Schedule")
197
+
198
+ # ---------- Booking ----------
199
+ gr.Markdown("## Book a Class")
200
+ with gr.Row():
201
+ class_id_in = gr.Number(label="Class ID")
202
+ client_id_in = gr.Textbox(label="Client ID")
203
+ cross_in = gr.Checkbox(label="Cross-regional booking", value=False)
204
+ book_out = gr.Textbox(label="Booking Result", lines=10, interactive=False)
205
+ book_btn = gr.Button("Book Class")
206
+
207
+ # ---------- Wiring ----------
208
+ auth_btn.click(fn=initialize_mindbody_client, outputs=auth_out)
209
+ types_btn.click(fn=get_training_types, inputs=loc_in, outputs=types_out)
210
+ sched_btn.click(fn=get_schedule, inputs=[loc_in, days_in], outputs=sched_out)
211
+ book_btn.click(fn=book_class_gr, inputs=[class_id_in, client_id_in, cross_in], outputs=book_out)
212
+
213
+ # -------------------------------------------------------------
214
+ # Launch
215
+ # -------------------------------------------------------------
216
+ if __name__ == "__main__":
217
+ # On HuggingFace or other MCP hosts, `mcp_server=True` makes Gradio bind
218
+ demo.launch(mcp_server=True)