remoterf 0.1.0.3__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.
remoteRF/core/app.py ADDED
@@ -0,0 +1,624 @@
1
+ from remoteRF.core.grpc_client import handle_admin_command
2
+ from . import *
3
+ from ..common.utils import *
4
+
5
+ import getpass
6
+ import os
7
+ import datetime
8
+ import time
9
+ import ast
10
+
11
+ from prompt_toolkit import PromptSession
12
+
13
+ account = RemoteRFAccount()
14
+ session = PromptSession()
15
+
16
+ def welcome():
17
+ printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), f"\nCurrent version: {print_my_version()} \nAll times are in Pacific Time (Los Angeles)", (Sty.GRAY))
18
+ try:
19
+ inpu = session.prompt(stylize("Please ", Sty.DEFAULT, "login", Sty.GREEN, " or ", Sty.DEFAULT, "register", Sty.RED, " to continue. (", Sty.DEFAULT, 'l', Sty.GREEN, "/", Sty.DEFAULT, 'r', Sty.RED, "): ", Sty.DEFAULT))
20
+ if inpu == 'r':
21
+ print("Registering new account ...")
22
+ account.username = input("Username: ")
23
+ double_check = True
24
+ while double_check:
25
+ password = getpass.getpass("Password (Hidden): ")
26
+ password2 = getpass.getpass("Confirm Password: ")
27
+ if password == password2:
28
+ double_check = False
29
+ else:
30
+ print("Passwords do not match. Try again.")
31
+
32
+ account.password = password
33
+ account.email = input("Email: ") # TODO: Email verification.
34
+ account.enrollment_code = input("Enrollment Code: ")
35
+ # check if login was valid
36
+ os.system('cls' if os.name == 'nt' else 'clear')
37
+
38
+ if not account.create_user():
39
+ welcome()
40
+ else:
41
+ account.username = input("Username: ")
42
+ account.password = getpass.getpass("Password (Hidden): ")
43
+ # check if login was valid
44
+ if not account.login_user():
45
+ os.system('cls' if os.name == 'nt' else 'clear')
46
+ print("Invalid login. Try again. Contact admin(s) if you forgot your password.")
47
+ welcome()
48
+ except KeyboardInterrupt:
49
+ exit()
50
+ except EOFError:
51
+ exit()
52
+
53
+ def title():
54
+ printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), f"\nCurrent version: {print_my_version()} \nAll times are in Pacific Time (Los Angeles)", (Sty.GRAY))
55
+ # printf(f"Logged in as: ", Sty.DEFAULT, f'{account.username}', Sty.MAGENTA)
56
+ printf(f"Input ", Sty.DEFAULT, "'help' ", Sty.BRIGHT_GREEN, "for a list of avaliable commands.", Sty.DEFAULT)
57
+
58
+ def commands():
59
+ printf("Commands:", Sty.BOLD)
60
+ printf("'clear' ", Sty.MAGENTA, " : Clear terminal", Sty.DEFAULT)
61
+ printf("'getdev' ", Sty.MAGENTA, " : View devices", Sty.DEFAULT)
62
+ printf("'help' or 'h' ", Sty.MAGENTA, " : Show this help message", Sty.DEFAULT)
63
+ printf("'perms' ", Sty.MAGENTA, " : View permissions", Sty.DEFAULT)
64
+ printf("'enroll' ", Sty.MAGENTA, " : Enroll with an enrollment code", Sty.DEFAULT)
65
+ printf("'exit' or 'quit' ", Sty.MAGENTA, ": Exit", Sty.DEFAULT)
66
+ printf("'getres' ", Sty.MAGENTA, " : View all reservations", Sty.DEFAULT)
67
+ printf("'myres' ", Sty.MAGENTA, " : View my reservations", Sty.DEFAULT)
68
+ printf("'cancelres' ", Sty.MAGENTA, " : Cancel a reservation", Sty.DEFAULT)
69
+ printf("'resdev' ", Sty.MAGENTA, " : Reserve a device", Sty.DEFAULT)
70
+ # printf("'resdev -n' ", Sty.MAGENTA, "- naive reserve device", Sty.DEFAULT)
71
+ # printf("'resdev s' ", Sty.MAGENTA, "- Reserve a Device (by single date)", Sty.DEFAULT)
72
+
73
+ # if account.is_admin:
74
+ # print()
75
+ # printf("Admin Commands:", Sty.BOLD)
76
+ # printf("'admin printa' ", Sty.MAGENTA, " : Print all accounts", Sty.DEFAULT)
77
+ # printf("'admin printr' ", Sty.MAGENTA, " : Print all reservations", Sty.DEFAULT)
78
+ # printf("'admin printp' ", Sty.MAGENTA, " : Print all perms", Sty.DEFAULT)
79
+ # printf("'admin printd' ", Sty.MAGENTA, " : Print all devices", Sty.DEFAULT)
80
+ # printf("'admin rm a <username>' ", Sty.MAGENTA, " : Remove one account", Sty.DEFAULT)
81
+ # printf("'admin rm aa' ", Sty.MAGENTA, " : Remove all accounts", Sty.DEFAULT)
82
+ # printf("'admin rm ar' ", Sty.MAGENTA, " : Remove all reservations", Sty.DEFAULT)
83
+ # # If you expose set_account remotely:
84
+ # printf("'admin setacc <username> <U|P|A> [args...]' ", Sty.MAGENTA, " : Set perms", Sty.DEFAULT)
85
+
86
+
87
+ def clear():
88
+ os.system('cls' if os.name == 'nt' else 'clear')
89
+ title()
90
+
91
+ def print_my_version():
92
+ import sys
93
+ latest = newest_version_pip("remoterf")
94
+ try:
95
+ import importlib.metadata as md # Py3.8+
96
+ top = __name__.split('.')[0]
97
+ # Try mapping package → distribution (Py3.10+); fall back to same name.
98
+ for dist in getattr(md, "packages_distributions", lambda: {})().get(top, []):
99
+ if (latest == md.version(dist)):
100
+ return f"{md.version(dist)} (LATEST)"
101
+ else:
102
+ return f"{md.version(dist)} (OUTDATED)"
103
+ return md.version(top)
104
+ except Exception:
105
+ # Last resort: __version__ attribute if you define it.
106
+ return getattr(sys.modules.get(__name__.split('.')[0]), "__version__", "unknown")
107
+
108
+ def newest_version_pip(project="remoterf"):
109
+ import sys, subprocess, re
110
+ out = subprocess.check_output(
111
+ [sys.executable, "-m", "pip", "index", "versions", project],
112
+ text=True, stderr=subprocess.STDOUT
113
+ )
114
+ m = re.search(r"(?i)\blatest\s*:\s*([^\s,]+)", out)
115
+ return m.group(1) if m else None
116
+
117
+
118
+ def reservations():
119
+ data = account.get_reservations()
120
+ if 'ace' in data.results:
121
+ print(f"Error: {unmap_arg(data.results['ace'])}")
122
+ return
123
+ entries = []
124
+
125
+ for key, value in data.results.items():
126
+ parts = unmap_arg(value).split(',')
127
+ # Create a dictionary for each entry with named fields
128
+ entry = {
129
+ 'username': parts[0],
130
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
131
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
132
+ 'end_time': parts[3]
133
+ }
134
+ entries.append(entry)
135
+
136
+ if (entries == []):
137
+ printf("No reservations found.", Sty.BOLD)
138
+ return
139
+
140
+ printf("Reservations:", Sty.BOLD)
141
+
142
+ # Sort the entries by device_id and then by start_time
143
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time']))
144
+
145
+ # Format the sorted entries into strings
146
+ for entry in sorted_entries:
147
+ printf(f'Device ID: ', Sty.RED, f'{entry["device_id"]}', Sty.MAGENTA, f', Start Time: ', Sty.RED, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f', End Time: ', Sty.RED, f'{entry["end_time"]}', Sty.BLUE)
148
+
149
+ def my_reservations():
150
+ data = account.get_reservations()
151
+ if 'ace' in data.results:
152
+ print(f"Error: {unmap_arg(data.results['ace'])}")
153
+ return
154
+ entries = []
155
+
156
+ for key, value in data.results.items():
157
+ parts = unmap_arg(value).split(',')
158
+ # Create a dictionary for each entry with named fields
159
+ entry = {
160
+ 'username': parts[0],
161
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
162
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
163
+ 'end_time': parts[3]
164
+ }
165
+ entries.append(entry)
166
+
167
+ if (entries == []):
168
+ printf("No reservations found.", Sty.BOLD)
169
+ return
170
+
171
+ printf("Reservations under: ", Sty.BOLD, f'{account.username}', Sty.MAGENTA)
172
+
173
+ # Sort the entries by device_id and then by start_time
174
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time']))
175
+
176
+ for entry in sorted_entries:
177
+ if account.username == entry['username']:
178
+ printf(f'Device ID: ', Sty.RED, f'{entry["device_id"]}', Sty.MAGENTA, f', Start Time: ', Sty.RED, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f', End Time: ', Sty.RED, f'{entry["end_time"]}', Sty.BLUE)
179
+
180
+ def cancel_my_reservation():
181
+ ## print all of ur reservations and their ids
182
+ ## ask for id to cancel
183
+ ## remove said reservation
184
+ data = account.get_reservations()
185
+ if 'ace' in data.results:
186
+ print(f"Error: {unmap_arg(data.results['ace'])}")
187
+ return
188
+
189
+ entries:list = []
190
+
191
+ for key, value in data.results.items():
192
+ parts = unmap_arg(value).split(',')
193
+ # Create a dictionary for each entry with named fields
194
+ entry = {
195
+ 'id': -1,
196
+ 'internal_id': key,
197
+ 'username': parts[0],
198
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
199
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
200
+ 'end_time': parts[3]
201
+ }
202
+ if account.username == entry['username']:
203
+ entries.append(entry)
204
+
205
+ printf("Current Reservation(s) under ", Sty.BOLD, f'{account.username}:', Sty.MAGENTA)
206
+
207
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time'])) # sort by device_id and start_time
208
+ for i, entry in enumerate(sorted_entries): # label all reservations with unique id
209
+ entry['id'] = i
210
+ printf(f'Reservation ID: ', Sty.GRAY, f'{i}', Sty.MAGENTA, f' Device ID: ', Sty.GRAY, f'{entry["device_id"]}', Sty.BRIGHT_GREEN, f' Start Time: ', Sty.GRAY, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f' End Time: ', Sty.GRAY, f'{entry["end_time"]}', Sty.BLUE)
211
+ # print(f"Reservation ID {i}, Device ID: {entry['device_id']}, Start Time: {entry['start_time'].strftime('%Y-%m-%d %H:%M:%S')}, End Time: {entry['end_time']}")
212
+
213
+ if sorted_entries == []:
214
+ printf("No reservations found.", Sty.BOLD)
215
+ return
216
+
217
+ inpu = session.prompt(stylize("Enter the ID of the reservation you would like to cancel ", Sty.BOLD, '(abort with any non number key input)', Sty.RED, ': ', Sty.BOLD))
218
+
219
+ if inpu.isdigit():
220
+ id = int(inpu)
221
+ if id >= len(sorted_entries):
222
+ print("Invalid ID.")
223
+ return
224
+
225
+ # grab the reservation
226
+ for entry in sorted_entries:
227
+ if entry['id'] == id:
228
+ db_id = entry['internal_id']
229
+ if session.prompt(stylize(f'Cancel reservation ID ', Sty.DEFAULT, f'{id}', Sty.MAGENTA, f' Device ID: ', Sty.DEFAULT, f'{entry["device_id"]}', Sty.BRIGHT_GREEN, f' Start Time: ', Sty.GRAY, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f' End Time: ', Sty.DEFAULT, f'{entry["end_time"]}', Sty.BLUE, f' ? (y/n):', Sty.DEFAULT)) == 'y':
230
+ response = account.cancel_reservation(db_id)
231
+ if 'ace' in response.results:
232
+ print(f"Error: {unmap_arg(response.results['ace'])}")
233
+ elif 'UC' in response.results:
234
+ printf(f"Reservation ID ", Sty.DEFAULT, f'{id}', Sty.BRIGHT_BLUE, ' successfully canceled.', Sty.DEFAULT)
235
+ else:
236
+ print("Aborting. User canceled action.")
237
+ return
238
+
239
+ print(f"Error: No reservation found with ID {id}.")
240
+ else:
241
+ print("Aborting. A non integer key was given.")
242
+
243
+ def devices():
244
+ data = account.get_devices()
245
+ if 'ace' in data.results:
246
+ print(f"Error: {unmap_arg(data.results['ace'])}")
247
+ return
248
+ printf("Devices:", Sty.BOLD)
249
+
250
+ for key in sorted(data.results, key=int):
251
+ printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{unmap_arg(data.results[key])}", Sty.GRAY)
252
+
253
+ def get_datetime(question:str):
254
+ timestamp = session.prompt(stylize(f'{question}', Sty.DEFAULT, ' (YYYY-MM-DD HH:MM): ', Sty.GRAY))
255
+ return datetime.datetime.strptime(timestamp + ':00', '%Y-%m-%d %H:%M:%S')
256
+
257
+ def reserve():
258
+ try:
259
+ id = session.prompt(stylize("Enter the device ID you would like to reserve: ", Sty.DEFAULT))
260
+ token = account.reserve_device(int(id), get_datetime("Reserve Start Time"), get_datetime("Reserve End Time"))
261
+ if token != '':
262
+ printf(f"Reservation successful. Thy Token -> ", Sty.BOLD, f"{token}", Sty.BG_GREEN)
263
+ printf(f"Please keep this token safe, as it is not saved on server side, and cannot be regenerated/reretrieved. ", Sty.DEFAULT)
264
+ except Exception as e:
265
+ printf(f"Error: {e}", Sty.BRIGHT_RED)
266
+
267
+ import ast
268
+ import json
269
+ def perms():
270
+ data = account.get_perms()
271
+ if 'ace' in data.results:
272
+ print(f"Error: {unmap_arg(data.results['ace'])}")
273
+ return
274
+
275
+ results = ast.literal_eval(unmap_arg(data.results['UC']))[0]
276
+ perm_level = results[0]
277
+
278
+ printf("Permission Level: ", Sty.BOLD, f"{perm_level}", Sty.BLUE)
279
+
280
+ if perm_level == "Normal User":
281
+ details_raw = unmap_arg(data.results.get("details", map_arg("{}")))
282
+ try:
283
+ details = json.loads(details_raw) if details_raw else {}
284
+ except Exception:
285
+ details = {}
286
+
287
+ devices = details.get("devices", []) or []
288
+ caps = details.get("caps", {}) or {}
289
+ groups = details.get("groups", []) or []
290
+
291
+ def _cap_for(dev_id: int):
292
+ return caps.get(str(dev_id)) or caps.get(dev_id) or {}
293
+
294
+ # ---- Groups (NEW) ----
295
+ if groups:
296
+ # keep stable ordering
297
+ groups = [str(g) for g in groups if str(g).strip() != ""]
298
+ print("User Groups:")
299
+ for g in groups:
300
+ print(f" - {g}")
301
+ else:
302
+ print("User Groups: (none)")
303
+
304
+ # ---- Devices ----
305
+ if not devices:
306
+ printf("Devices: ", Sty.DEFAULT, "None", Sty.MAGENTA)
307
+ return
308
+
309
+ printf("Devices allowed: ", Sty.DEFAULT, f"{devices}", Sty.MAGENTA)
310
+
311
+ # Build per-device caps and group identical limits together
312
+ buckets: dict[tuple[int, int], list[int]] = {} # (max_r, max_t_sec) -> [dev_ids]
313
+ for d in devices:
314
+ try:
315
+ did = int(d)
316
+ except Exception:
317
+ continue
318
+ c = _cap_for(did)
319
+ max_t = int(c.get("max_reservation_time_sec", 0) or 0)
320
+ max_r = int(c.get("max_reservations", 0) or 0)
321
+ buckets.setdefault((max_r, max_t), []).append(did)
322
+
323
+ if not buckets:
324
+ print("Limits per device: (none)")
325
+ return
326
+
327
+ # If everything shares the same limits, print once
328
+ if len(buckets) == 1:
329
+ (max_r, max_t), _devs = next(iter(buckets.items()))
330
+ print("Limits (all devices):")
331
+ print(f" Max Concurrent Reservations: {max_r}")
332
+ print(f" Max Reservation Duration (min): {max_t // 60}")
333
+ return
334
+
335
+ # Otherwise print grouped limits
336
+ print("Limits per device (grouped):")
337
+ for (max_r, max_t), devs in sorted(buckets.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[1])):
338
+ devs = sorted(devs)
339
+
340
+ # compress ranges like 0-3,5,7-9
341
+ ranges = []
342
+ start = prev = None
343
+ for x in devs:
344
+ if start is None:
345
+ start = prev = x
346
+ continue
347
+ if x == prev + 1:
348
+ prev = x
349
+ else:
350
+ ranges.append(f"{start}-{prev}" if start != prev else f"{start}")
351
+ start = prev = x
352
+ if start is not None:
353
+ ranges.append(f"{start}-{prev}" if start != prev else f"{start}")
354
+
355
+ dev_str = ",".join(ranges)
356
+ print(f" devices[{dev_str}]: max_reservations={max_r}, max_time_min={max_t // 60}")
357
+
358
+ elif perm_level == "Power User":
359
+ printf("Max Reservations: ", Sty.DEFAULT, f"{results[3]}", Sty.MAGENTA)
360
+ printf("Max Reservation Duration (min): ", Sty.DEFAULT, f"{int(results[4]/60)}", Sty.MAGENTA)
361
+ printf("Device IDs allowed Access to: ", Sty.DEFAULT, f"{results[5]}", Sty.MAGENTA)
362
+
363
+ elif perm_level == "Admin":
364
+ printf("No restrictions on reservation count or duration.", Sty.DEFAULT)
365
+
366
+ else:
367
+ printf(f"Error: Unknown permission level {perm_level}", Sty.BRIGHT_RED)
368
+
369
+
370
+ def enroll():
371
+ code = session.prompt(stylize("Enter your enrollment code: ", Sty.DEFAULT))
372
+ account.enrollment_code = code
373
+ data = account.set_enroll()
374
+
375
+ if 'ace' in data.results:
376
+ print(f"Error: {unmap_arg(data.results['ace'])}")
377
+ return
378
+ else:
379
+ # todo: print out specifics, like group name (ie: successfully enrolled in ECE 132A)
380
+ printf("Enrollment successful.", Sty.BG_GREEN)
381
+
382
+ # New block scheduling
383
+
384
+ def fetch_all_reservations():
385
+ data = account.get_reservations()
386
+ if 'ace' in data.results:
387
+ print(f"Error: {unmap_arg(data.results['ace'])}")
388
+ return []
389
+ entries = []
390
+
391
+ for key, value in data.results.items():
392
+ parts = unmap_arg(value).split(',')
393
+ # Convert both start and end times to datetime objects.
394
+ entry = {
395
+ 'username': parts[0],
396
+ 'device_id': int(parts[1]), # Stored as an int
397
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'),
398
+ 'end_time': datetime.datetime.strptime(parts[3], '%Y-%m-%d %H:%M:%S')
399
+ }
400
+ entries.append(entry)
401
+ return entries
402
+
403
+ def fetch_reservations_for_range(start_day: datetime.date, end_day: datetime.date):
404
+ """
405
+ Fetch all reservations (via fetch_all_reservations) and filter those whose start_time date falls between start_day and end_day (inclusive).
406
+ Returns a dictionary keyed by (device_id, day) (device_id as string, day as datetime.date) with a list of reservation tuples.
407
+ """
408
+ all_res = fetch_all_reservations() # This calls the network only once.
409
+ res_dict = {}
410
+ for res in all_res:
411
+ res_day = res['start_time'].date()
412
+ if start_day <= res_day <= end_day:
413
+ key = (str(res['device_id']), res_day)
414
+ res_dict.setdefault(key, []).append((res['start_time'], res['end_time']))
415
+ return res_dict
416
+
417
+ def is_slot_conflicting(slot: tuple, reservations: list):
418
+ """Return True if the slot overlaps with any reservation in the provided list."""
419
+ slot_start, slot_end = slot
420
+ for res_start, res_end in reservations:
421
+ if slot_start < res_end and slot_end > res_start:
422
+ return True
423
+ return False
424
+
425
+ def interactive_reserve_next_days(block_minutes=60):
426
+ """
427
+ Interactive function that:
428
+ 1) Displays a menu of all devices (0-based indexing).
429
+ 2) Prompts the user for which device they want.
430
+ 3) Prompts how many days (starting today) to check for available reservations.
431
+ 4) Prompts for an optional block duration in minutes (default = 60).
432
+ 5) Displays the free time slots (in 'block_minutes' increments) for that device,
433
+ over the indicated number of days, also 0-based indexed.
434
+ 6) Reserves the chosen slot on that device, after confirmation.
435
+ """
436
+ try:
437
+ # --- 1) Fetch and display all devices ---
438
+ data = account.get_devices()
439
+ if 'ace' in data.results:
440
+ print(f"Error: {unmap_arg(data.results['ace'])}")
441
+ return
442
+
443
+ print("Devices:")
444
+ # Sort devices by integer key
445
+ sorted_device_ids = sorted(data.results.keys(), key=int)
446
+
447
+ for idx, dev_id in enumerate(sorted_device_ids):
448
+ dev_name = unmap_arg(data.results[dev_id])
449
+ print(f"{idx}. Device ID: {dev_id} Name: {dev_name}")
450
+
451
+ # --- 2) Prompt user to pick a device by 0-based index ---
452
+ device_selection = input("Which device do you want? (enter the 0-based index): ")
453
+ try:
454
+ device_selection = int(device_selection)
455
+ if device_selection < 0 or device_selection >= len(sorted_device_ids):
456
+ print("Invalid selection.")
457
+ return
458
+ except ValueError:
459
+ print("Invalid input. Please enter a number.")
460
+ return
461
+
462
+ chosen_device_id = sorted_device_ids[device_selection]
463
+
464
+ # --- 3) Prompt user for the number of days ---
465
+ num_days = int(input("Enter the number of days to check for available reservations (starting today): "))
466
+
467
+ # --- 4) Optionally override block_minutes ---
468
+ # user_block_input = input(f"Enter block duration in minutes (e.g. 15, 30, 60, 120). Press Enter to default ({block_minutes}): ").strip()
469
+ # if user_block_input:
470
+ # try:
471
+ # block_minutes = int(user_block_input)
472
+ # except ValueError:
473
+ # print("Invalid block duration. Using default of 60 minutes.")
474
+ # block_minutes = 60
475
+
476
+ # --- 5) Find all free time slots for the chosen device over the next `num_days` days ---
477
+
478
+ today = datetime.date.today()
479
+ end_day = today + datetime.timedelta(days=num_days - 1)
480
+
481
+ # We only need one call to fetch reservations for the range:
482
+ reservations_range = fetch_reservations_for_range(today, end_day)
483
+
484
+ # We'll keep a list of (day, (slot_start, slot_end)) for which the device is free
485
+ available_slots = []
486
+
487
+ now = datetime.datetime.now()
488
+
489
+ # Helper to build time slots of length 'block_minutes' starting at 00:00 up to 24:00
490
+ def build_time_slots(date: datetime.date, block_size: int):
491
+ slots = []
492
+ start_of_day = datetime.datetime.combine(date, datetime.time(0, 0))
493
+ minutes_in_day = 24 * 60 # 1440
494
+ current_offset = 0
495
+ while current_offset < minutes_in_day:
496
+ slot_start = start_of_day + datetime.timedelta(minutes=current_offset)
497
+ slot_end = slot_start + datetime.timedelta(minutes=block_size)
498
+ # Stop if slot_end bleeds into the next calendar day
499
+ if slot_end.date() != date and slot_end.time() != datetime.time.min:
500
+ break
501
+ slots.append((slot_start, slot_end))
502
+ current_offset += block_size
503
+ return slots
504
+
505
+ # Build free slots for each day in [today, end_day]
506
+ for i in range(num_days):
507
+ day = today + datetime.timedelta(days=i)
508
+ all_slots = build_time_slots(day, block_minutes)
509
+
510
+ # The reservations for the chosen device on this day:
511
+ key = (str(chosen_device_id), day)
512
+ day_reservations = reservations_range.get(key, [])
513
+
514
+ for slot in all_slots:
515
+ slot_start, slot_end = slot
516
+ # Skip if it's for "today" and the slot ends in the past
517
+ if day == today and slot_end <= now:
518
+ continue
519
+
520
+ # Check for conflict
521
+ if not is_slot_conflicting(slot, day_reservations):
522
+ available_slots.append((day, slot))
523
+
524
+ if not available_slots:
525
+ print(f"No available time slots for device {chosen_device_id} in the next {num_days} days.")
526
+ return
527
+
528
+ # Sort by day, then by slot start time
529
+ available_slots.sort(key=lambda x: (x[0], x[1][0]))
530
+
531
+ # --- Display the available slots, using 0-based index ---
532
+ print(f"\nAvailable time slots for device {chosen_device_id} over the next {num_days} days:")
533
+ last_day = None
534
+ for idx, (day, slot) in enumerate(available_slots):
535
+ slot_start_str = slot[0].strftime('%I:%M %p')
536
+ slot_end_str = slot[1].strftime('%I:%M %p')
537
+ if day != last_day:
538
+ # Print a header for the day
539
+ day_header = f"{day.strftime('%Y-%m-%d')} ({day.strftime('%a')}) {day.strftime('%b')}. {day.day}"
540
+ print("\n" + day_header)
541
+ last_day = day
542
+
543
+ print(f" {idx}. {slot_start_str} - {slot_end_str}")
544
+
545
+ # Prompt user to pick a slot by 0-based index
546
+ selection = input("Select a slot by index: ")
547
+ try:
548
+ selection = int(selection)
549
+ if selection < 0 or selection >= len(available_slots):
550
+ print("Invalid selection.")
551
+ return
552
+ except ValueError:
553
+ print("Invalid input. Please enter a number.")
554
+ return
555
+
556
+ chosen_day, chosen_slot = available_slots[selection]
557
+ slot_start_str = chosen_slot[0].strftime('%I:%M %p')
558
+ slot_end_str = chosen_slot[1].strftime('%I:%M %p')
559
+
560
+ confirmation = input(
561
+ f"You have selected a reservation on {chosen_day.strftime('%Y-%m-%d')} "
562
+ f"from {slot_start_str} to {slot_end_str} on device {chosen_device_id}. "
563
+ f"Confirm reservation? (y/n): "
564
+ ).strip().lower()
565
+
566
+ if confirmation != 'y':
567
+ print("Reservation cancelled.")
568
+ return
569
+
570
+ # print(f"device_id : {chosen_device_id}, start_time : {chosen_slot[0]}, end_time : {chosen_slot[1]}")
571
+
572
+ # --- 6) Reserve the chosen slot on the chosen device ---
573
+ token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
574
+ if token:
575
+ print(f"Reservation successful on device {chosen_device_id} for "
576
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.")
577
+ print(f"Thy Token -> {token}")
578
+ print("Please keep this token safe, as it is not saved on server side "
579
+ "and cannot be retrieved again.")
580
+
581
+ except Exception as e:
582
+ print(f"Error: {e}")
583
+
584
+ welcome()
585
+ clear()
586
+
587
+ while True:
588
+ try:
589
+ inpu = session.prompt(stylize(f'{account.username}@remote_rf: ', Sty.BLUE))
590
+ if inpu == "clear":
591
+ clear()
592
+ elif inpu == "getdev":
593
+ devices()
594
+ elif inpu == "help" or inpu == "h":
595
+ commands()
596
+ elif inpu == "perms":
597
+ perms()
598
+ elif inpu == "enroll":
599
+ enroll()
600
+ elif inpu == "quit" or inpu == "exit":
601
+ break
602
+ elif inpu == "getres":
603
+ reservations()
604
+ elif inpu == "myres":
605
+ my_reservations()
606
+ # elif inpu == "resdev s":
607
+ # interactive_reserve_all()
608
+ elif inpu == "resdev":
609
+ interactive_reserve_next_days(block_minutes=30)
610
+ elif inpu == 'cancelres':
611
+ cancel_my_reservation()
612
+
613
+ elif inpu == 'resdev -n':
614
+ # check if user is admin
615
+ # if account.get_perms().results['UC'] == 'Admin':
616
+ reserve()
617
+ elif account.is_admin and inpu.strip().startswith("admin"):
618
+ handle_admin_command(inpu)
619
+ else:
620
+ print(f"Unknown command: {inpu}")
621
+ except KeyboardInterrupt:
622
+ break
623
+ except EOFError:
624
+ break
@@ -0,0 +1,60 @@
1
+ import ast
2
+ from .grpc_client import rpc_client
3
+ from ..common.utils import *
4
+
5
+ import datetime
6
+
7
+ class RemoteRFAccount:
8
+ def __init__(self, username:str=None, password:str=None, email:str=None):
9
+ self.username = username
10
+ self.password = password
11
+ self.email = email
12
+ self.enrollment_code = ""
13
+ self.is_admin = False
14
+
15
+ def create_user(self):
16
+ response = rpc_client(function_name="ACC:create_user", args={"un":map_arg(self.username), "pw":map_arg(self.password), "em":map_arg(self.email), "ec":map_arg(self.enrollment_code)})
17
+ if 'UC' in response.results:
18
+ print(f'User {unmap_arg(response.results["UC"])} successfully created.')
19
+ return True
20
+ elif 'UE' in response.results:
21
+ print(f'Error: {unmap_arg(response.results["UE"])}')
22
+ return False
23
+
24
+ def login_user(self):
25
+ username = self.username
26
+ password = self.password
27
+ response = rpc_client(function_name="ACC:login", args={"un":map_arg(username), "pw":map_arg(password)})
28
+ if 'UC' in response.results:
29
+ print(f'User {unmap_arg(response.results["UC"])} successful login.')
30
+
31
+ self.is_admin = (ast.literal_eval(unmap_arg(self.get_perms().results['UC']))[0][0] == 'Admin')
32
+ return True
33
+ elif 'UE' in response.results:
34
+ print(f'Error: {unmap_arg(response.results["UE"])}')
35
+ return False
36
+
37
+ def reserve_device(self, device_id:int, start_time:datetime, end_time:datetime):
38
+ response = rpc_client(function_name="ACC:reserve_device", args={"un":map_arg(self.username), "pw":map_arg(self.password), "dd":map_arg(device_id), "st":map_arg(int(start_time.timestamp())), "et":map_arg(int(end_time.timestamp()))})
39
+
40
+ if 'ace' in response.results:
41
+ raise Exception(f'{unmap_arg(response.results["ace"])}')
42
+ elif 'Token' in response.results:
43
+ return unmap_arg(response.results["Token"])
44
+
45
+ def get_reservations(self):
46
+ return rpc_client(function_name='ACC:get_res', args={"un":map_arg(self.username), "pw":map_arg(self.password)})
47
+
48
+ def get_devices(self):
49
+ return rpc_client(function_name='ACC:get_dev', args={"un":map_arg(self.username), "pw":map_arg(self.password)})
50
+
51
+ def cancel_reservation(self, res_id:int):
52
+ return rpc_client(function_name='ACC:cancel_res', args={"un":map_arg(self.username), "pw":map_arg(self.password), "res_id":map_arg(res_id)})
53
+
54
+ def get_perms(self):
55
+ return rpc_client(function_name='ACC:get_perms', args={"un":map_arg(self.username), "pw":map_arg(self.password)})
56
+
57
+ def set_enroll(self):
58
+ return rpc_client(function_name='ACC:set_enroll', args={"un":map_arg(self.username), "pw":map_arg(self.password), "ec":map_arg(self.enrollment_code)})
59
+
60
+