pltr-cli 0.4.0__py3-none-any.whl → 0.5.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,642 @@
1
+ """
2
+ Orchestration commands for managing builds, jobs, and schedules.
3
+ """
4
+
5
+ import typer
6
+ import json
7
+ from typing import Optional
8
+ from rich.console import Console
9
+
10
+ from ..services.orchestration import OrchestrationService
11
+ from ..utils.formatting import OutputFormatter
12
+ from ..utils.progress import SpinnerProgressTracker
13
+ from ..auth.base import ProfileNotFoundError, MissingCredentialsError
14
+ from ..utils.completion import (
15
+ complete_rid,
16
+ complete_profile,
17
+ complete_output_format,
18
+ cache_rid,
19
+ )
20
+
21
+ app = typer.Typer()
22
+ console = Console()
23
+ formatter = OutputFormatter(console)
24
+
25
+ # Create sub-apps for different orchestration components
26
+ builds_app = typer.Typer()
27
+ jobs_app = typer.Typer()
28
+ schedules_app = typer.Typer()
29
+
30
+ # ============================================================================
31
+ # Build Commands
32
+ # ============================================================================
33
+
34
+
35
+ @builds_app.command("get")
36
+ def get_build(
37
+ build_rid: str = typer.Argument(
38
+ ..., help="Build Resource Identifier", autocompletion=complete_rid
39
+ ),
40
+ profile: Optional[str] = typer.Option(
41
+ None, "--profile", "-p", help="Profile name", autocompletion=complete_profile
42
+ ),
43
+ format: str = typer.Option(
44
+ "table",
45
+ "--format",
46
+ "-f",
47
+ help="Output format (table, json, csv)",
48
+ autocompletion=complete_output_format,
49
+ ),
50
+ output: Optional[str] = typer.Option(
51
+ None, "--output", "-o", help="Output file path"
52
+ ),
53
+ ):
54
+ """Get detailed information about a specific build."""
55
+ try:
56
+ cache_rid(build_rid)
57
+ service = OrchestrationService(profile=profile)
58
+
59
+ with SpinnerProgressTracker().track_spinner(f"Fetching build {build_rid}..."):
60
+ build = service.get_build(build_rid)
61
+
62
+ formatter.format_build_detail(build, format, output)
63
+
64
+ if output:
65
+ formatter.print_success(f"Build information saved to {output}")
66
+
67
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
68
+ formatter.print_error(f"Authentication error: {e}")
69
+ raise typer.Exit(1)
70
+ except Exception as e:
71
+ formatter.print_error(f"Failed to get build: {e}")
72
+ raise typer.Exit(1)
73
+
74
+
75
+ @builds_app.command("create")
76
+ def create_build(
77
+ target: str = typer.Argument(..., help="Build target (JSON format)"),
78
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
79
+ branch_name: Optional[str] = typer.Option(
80
+ None, "--branch", help="Branch name for the build"
81
+ ),
82
+ force_build: bool = typer.Option(
83
+ False, "--force", help="Force build even if no changes"
84
+ ),
85
+ abort_on_failure: bool = typer.Option(
86
+ False, "--abort-on-failure", help="Abort on failure"
87
+ ),
88
+ notifications: bool = typer.Option(
89
+ True, "--notifications/--no-notifications", help="Enable notifications"
90
+ ),
91
+ format: str = typer.Option(
92
+ "table", "--format", "-f", help="Output format (table, json, csv)"
93
+ ),
94
+ ):
95
+ """Create a new build."""
96
+ try:
97
+ service = OrchestrationService(profile=profile)
98
+
99
+ # Parse target JSON
100
+ try:
101
+ target_dict = json.loads(target)
102
+ except json.JSONDecodeError:
103
+ formatter.print_error("Invalid JSON format for target")
104
+ raise typer.Exit(1)
105
+
106
+ with SpinnerProgressTracker().track_spinner("Creating build..."):
107
+ build = service.create_build(
108
+ target=target_dict,
109
+ branch_name=branch_name,
110
+ force_build=force_build,
111
+ abort_on_failure=abort_on_failure,
112
+ notifications_enabled=notifications,
113
+ )
114
+
115
+ formatter.print_success("Successfully created build")
116
+ formatter.print_info(f"Build RID: {build.get('rid', 'unknown')}")
117
+ formatter.format_build_detail(build, format)
118
+
119
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
120
+ formatter.print_error(f"Authentication error: {e}")
121
+ raise typer.Exit(1)
122
+ except Exception as e:
123
+ formatter.print_error(f"Failed to create build: {e}")
124
+ raise typer.Exit(1)
125
+
126
+
127
+ @builds_app.command("cancel")
128
+ def cancel_build(
129
+ build_rid: str = typer.Argument(
130
+ ..., help="Build Resource Identifier", autocompletion=complete_rid
131
+ ),
132
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
133
+ ):
134
+ """Cancel a build and all its unfinished jobs."""
135
+ try:
136
+ service = OrchestrationService(profile=profile)
137
+
138
+ with SpinnerProgressTracker().track_spinner(f"Cancelling build {build_rid}..."):
139
+ service.cancel_build(build_rid)
140
+
141
+ formatter.print_success(f"Successfully cancelled build {build_rid}")
142
+
143
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
144
+ formatter.print_error(f"Authentication error: {e}")
145
+ raise typer.Exit(1)
146
+ except Exception as e:
147
+ formatter.print_error(f"Failed to cancel build: {e}")
148
+ raise typer.Exit(1)
149
+
150
+
151
+ @builds_app.command("jobs")
152
+ def get_build_jobs(
153
+ build_rid: str = typer.Argument(
154
+ ..., help="Build Resource Identifier", autocompletion=complete_rid
155
+ ),
156
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
157
+ page_size: Optional[int] = typer.Option(
158
+ None, "--page-size", help="Number of results per page"
159
+ ),
160
+ format: str = typer.Option(
161
+ "table", "--format", "-f", help="Output format (table, json, csv)"
162
+ ),
163
+ output: Optional[str] = typer.Option(
164
+ None, "--output", "-o", help="Output file path"
165
+ ),
166
+ ):
167
+ """List all jobs in a build."""
168
+ try:
169
+ cache_rid(build_rid)
170
+ service = OrchestrationService(profile=profile)
171
+
172
+ with SpinnerProgressTracker().track_spinner(
173
+ f"Fetching jobs for build {build_rid}..."
174
+ ):
175
+ response = service.get_build_jobs(build_rid, page_size=page_size)
176
+
177
+ jobs = response.get("jobs", [])
178
+
179
+ if not jobs:
180
+ formatter.print_warning("No jobs found for this build")
181
+ return
182
+
183
+ formatter.format_jobs_list(jobs, format, output)
184
+
185
+ if output:
186
+ formatter.print_success(f"Jobs list saved to {output}")
187
+
188
+ if response.get("next_page_token"):
189
+ formatter.print_info(
190
+ "More results available. Use pagination token to fetch next page."
191
+ )
192
+
193
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
194
+ formatter.print_error(f"Authentication error: {e}")
195
+ raise typer.Exit(1)
196
+ except Exception as e:
197
+ formatter.print_error(f"Failed to get build jobs: {e}")
198
+ raise typer.Exit(1)
199
+
200
+
201
+ @builds_app.command("search")
202
+ def search_builds(
203
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
204
+ page_size: Optional[int] = typer.Option(
205
+ None, "--page-size", help="Number of results per page"
206
+ ),
207
+ format: str = typer.Option(
208
+ "table", "--format", "-f", help="Output format (table, json, csv)"
209
+ ),
210
+ output: Optional[str] = typer.Option(
211
+ None, "--output", "-o", help="Output file path"
212
+ ),
213
+ ):
214
+ """Search for builds."""
215
+ try:
216
+ service = OrchestrationService(profile=profile)
217
+
218
+ with SpinnerProgressTracker().track_spinner("Searching builds..."):
219
+ response = service.search_builds(page_size=page_size)
220
+
221
+ builds = response.get("builds", [])
222
+
223
+ if not builds:
224
+ formatter.print_warning("No builds found")
225
+ return
226
+
227
+ formatter.format_builds_list(builds, format, output)
228
+
229
+ if output:
230
+ formatter.print_success(f"Builds list saved to {output}")
231
+
232
+ if response.get("next_page_token"):
233
+ formatter.print_info(
234
+ "More results available. Use pagination token to fetch next page."
235
+ )
236
+
237
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
238
+ formatter.print_error(f"Authentication error: {e}")
239
+ raise typer.Exit(1)
240
+ except Exception as e:
241
+ formatter.print_error(f"Failed to search builds: {e}")
242
+ raise typer.Exit(1)
243
+
244
+
245
+ # ============================================================================
246
+ # Job Commands
247
+ # ============================================================================
248
+
249
+
250
+ @jobs_app.command("get")
251
+ def get_job(
252
+ job_rid: str = typer.Argument(
253
+ ..., help="Job Resource Identifier", autocompletion=complete_rid
254
+ ),
255
+ profile: Optional[str] = typer.Option(
256
+ None, "--profile", "-p", help="Profile name", autocompletion=complete_profile
257
+ ),
258
+ format: str = typer.Option(
259
+ "table",
260
+ "--format",
261
+ "-f",
262
+ help="Output format (table, json, csv)",
263
+ autocompletion=complete_output_format,
264
+ ),
265
+ output: Optional[str] = typer.Option(
266
+ None, "--output", "-o", help="Output file path"
267
+ ),
268
+ ):
269
+ """Get detailed information about a specific job."""
270
+ try:
271
+ cache_rid(job_rid)
272
+ service = OrchestrationService(profile=profile)
273
+
274
+ with SpinnerProgressTracker().track_spinner(f"Fetching job {job_rid}..."):
275
+ job = service.get_job(job_rid)
276
+
277
+ formatter.format_job_detail(job, format, output)
278
+
279
+ if output:
280
+ formatter.print_success(f"Job information saved to {output}")
281
+
282
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
283
+ formatter.print_error(f"Authentication error: {e}")
284
+ raise typer.Exit(1)
285
+ except Exception as e:
286
+ formatter.print_error(f"Failed to get job: {e}")
287
+ raise typer.Exit(1)
288
+
289
+
290
+ @jobs_app.command("get-batch")
291
+ def get_jobs_batch(
292
+ job_rids: str = typer.Argument(
293
+ ..., help="Comma-separated list of Job RIDs (max 500)"
294
+ ),
295
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
296
+ format: str = typer.Option(
297
+ "table", "--format", "-f", help="Output format (table, json, csv)"
298
+ ),
299
+ output: Optional[str] = typer.Option(
300
+ None, "--output", "-o", help="Output file path"
301
+ ),
302
+ ):
303
+ """Get multiple jobs in batch (max 500)."""
304
+ try:
305
+ # Parse job RIDs
306
+ rids_list = [rid.strip() for rid in job_rids.split(",")]
307
+
308
+ if len(rids_list) > 500:
309
+ formatter.print_error("Maximum batch size is 500 jobs")
310
+ raise typer.Exit(1)
311
+
312
+ service = OrchestrationService(profile=profile)
313
+
314
+ with SpinnerProgressTracker().track_spinner(
315
+ f"Fetching {len(rids_list)} jobs..."
316
+ ):
317
+ response = service.get_jobs_batch(rids_list)
318
+
319
+ jobs = response.get("jobs", [])
320
+
321
+ if not jobs:
322
+ formatter.print_warning("No jobs found")
323
+ return
324
+
325
+ formatter.format_jobs_list(jobs, format, output)
326
+
327
+ if output:
328
+ formatter.print_success(f"Jobs information saved to {output}")
329
+
330
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
331
+ formatter.print_error(f"Authentication error: {e}")
332
+ raise typer.Exit(1)
333
+ except Exception as e:
334
+ formatter.print_error(f"Failed to get jobs batch: {e}")
335
+ raise typer.Exit(1)
336
+
337
+
338
+ # ============================================================================
339
+ # Schedule Commands
340
+ # ============================================================================
341
+
342
+
343
+ @schedules_app.command("get")
344
+ def get_schedule(
345
+ schedule_rid: str = typer.Argument(
346
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
347
+ ),
348
+ profile: Optional[str] = typer.Option(
349
+ None, "--profile", "-p", help="Profile name", autocompletion=complete_profile
350
+ ),
351
+ preview: bool = typer.Option(False, "--preview", help="Enable preview mode"),
352
+ format: str = typer.Option(
353
+ "table",
354
+ "--format",
355
+ "-f",
356
+ help="Output format (table, json, csv)",
357
+ autocompletion=complete_output_format,
358
+ ),
359
+ output: Optional[str] = typer.Option(
360
+ None, "--output", "-o", help="Output file path"
361
+ ),
362
+ ):
363
+ """Get detailed information about a specific schedule."""
364
+ try:
365
+ cache_rid(schedule_rid)
366
+ service = OrchestrationService(profile=profile)
367
+
368
+ with SpinnerProgressTracker().track_spinner(
369
+ f"Fetching schedule {schedule_rid}..."
370
+ ):
371
+ schedule = service.get_schedule(schedule_rid, preview=preview)
372
+
373
+ formatter.format_schedule_detail(schedule, format, output)
374
+
375
+ if output:
376
+ formatter.print_success(f"Schedule information saved to {output}")
377
+
378
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
379
+ formatter.print_error(f"Authentication error: {e}")
380
+ raise typer.Exit(1)
381
+ except Exception as e:
382
+ formatter.print_error(f"Failed to get schedule: {e}")
383
+ raise typer.Exit(1)
384
+
385
+
386
+ @schedules_app.command("create")
387
+ def create_schedule(
388
+ action: str = typer.Argument(..., help="Schedule action (JSON format)"),
389
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
390
+ display_name: Optional[str] = typer.Option(
391
+ None, "--name", help="Display name for the schedule"
392
+ ),
393
+ description: Optional[str] = typer.Option(
394
+ None, "--description", help="Schedule description"
395
+ ),
396
+ trigger: Optional[str] = typer.Option(
397
+ None, "--trigger", help="Trigger configuration (JSON format)"
398
+ ),
399
+ preview: bool = typer.Option(False, "--preview", help="Enable preview mode"),
400
+ format: str = typer.Option(
401
+ "table", "--format", "-f", help="Output format (table, json, csv)"
402
+ ),
403
+ ):
404
+ """Create a new schedule."""
405
+ try:
406
+ service = OrchestrationService(profile=profile)
407
+
408
+ # Parse action JSON
409
+ try:
410
+ action_dict = json.loads(action)
411
+ except json.JSONDecodeError:
412
+ formatter.print_error("Invalid JSON format for action")
413
+ raise typer.Exit(1)
414
+
415
+ # Parse trigger JSON if provided
416
+ trigger_dict = None
417
+ if trigger:
418
+ try:
419
+ trigger_dict = json.loads(trigger)
420
+ except json.JSONDecodeError:
421
+ formatter.print_error("Invalid JSON format for trigger")
422
+ raise typer.Exit(1)
423
+
424
+ with SpinnerProgressTracker().track_spinner("Creating schedule..."):
425
+ schedule = service.create_schedule(
426
+ action=action_dict,
427
+ display_name=display_name,
428
+ description=description,
429
+ trigger=trigger_dict,
430
+ preview=preview,
431
+ )
432
+
433
+ formatter.print_success("Successfully created schedule")
434
+ formatter.print_info(f"Schedule RID: {schedule.get('rid', 'unknown')}")
435
+ formatter.format_schedule_detail(schedule, format)
436
+
437
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
438
+ formatter.print_error(f"Authentication error: {e}")
439
+ raise typer.Exit(1)
440
+ except Exception as e:
441
+ formatter.print_error(f"Failed to create schedule: {e}")
442
+ raise typer.Exit(1)
443
+
444
+
445
+ @schedules_app.command("delete")
446
+ def delete_schedule(
447
+ schedule_rid: str = typer.Argument(
448
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
449
+ ),
450
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
451
+ confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
452
+ ):
453
+ """Delete a schedule."""
454
+ try:
455
+ if not confirm:
456
+ typer.confirm(
457
+ f"Are you sure you want to delete schedule {schedule_rid}?", abort=True
458
+ )
459
+
460
+ service = OrchestrationService(profile=profile)
461
+
462
+ with SpinnerProgressTracker().track_spinner(
463
+ f"Deleting schedule {schedule_rid}..."
464
+ ):
465
+ service.delete_schedule(schedule_rid)
466
+
467
+ formatter.print_success(f"Successfully deleted schedule {schedule_rid}")
468
+
469
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
470
+ formatter.print_error(f"Authentication error: {e}")
471
+ raise typer.Exit(1)
472
+ except Exception as e:
473
+ formatter.print_error(f"Failed to delete schedule: {e}")
474
+ raise typer.Exit(1)
475
+
476
+
477
+ @schedules_app.command("pause")
478
+ def pause_schedule(
479
+ schedule_rid: str = typer.Argument(
480
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
481
+ ),
482
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
483
+ ):
484
+ """Pause a schedule."""
485
+ try:
486
+ service = OrchestrationService(profile=profile)
487
+
488
+ with SpinnerProgressTracker().track_spinner(
489
+ f"Pausing schedule {schedule_rid}..."
490
+ ):
491
+ service.pause_schedule(schedule_rid)
492
+
493
+ formatter.print_success(f"Successfully paused schedule {schedule_rid}")
494
+
495
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
496
+ formatter.print_error(f"Authentication error: {e}")
497
+ raise typer.Exit(1)
498
+ except Exception as e:
499
+ formatter.print_error(f"Failed to pause schedule: {e}")
500
+ raise typer.Exit(1)
501
+
502
+
503
+ @schedules_app.command("unpause")
504
+ def unpause_schedule(
505
+ schedule_rid: str = typer.Argument(
506
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
507
+ ),
508
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
509
+ ):
510
+ """Unpause a schedule."""
511
+ try:
512
+ service = OrchestrationService(profile=profile)
513
+
514
+ with SpinnerProgressTracker().track_spinner(
515
+ f"Unpausing schedule {schedule_rid}..."
516
+ ):
517
+ service.unpause_schedule(schedule_rid)
518
+
519
+ formatter.print_success(f"Successfully unpaused schedule {schedule_rid}")
520
+
521
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
522
+ formatter.print_error(f"Authentication error: {e}")
523
+ raise typer.Exit(1)
524
+ except Exception as e:
525
+ formatter.print_error(f"Failed to unpause schedule: {e}")
526
+ raise typer.Exit(1)
527
+
528
+
529
+ @schedules_app.command("run")
530
+ def run_schedule(
531
+ schedule_rid: str = typer.Argument(
532
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
533
+ ),
534
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
535
+ ):
536
+ """Execute a schedule immediately."""
537
+ try:
538
+ service = OrchestrationService(profile=profile)
539
+
540
+ with SpinnerProgressTracker().track_spinner(
541
+ f"Running schedule {schedule_rid}..."
542
+ ):
543
+ service.run_schedule(schedule_rid)
544
+
545
+ formatter.print_success(f"Successfully triggered schedule {schedule_rid}")
546
+
547
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
548
+ formatter.print_error(f"Authentication error: {e}")
549
+ raise typer.Exit(1)
550
+ except Exception as e:
551
+ formatter.print_error(f"Failed to run schedule: {e}")
552
+ raise typer.Exit(1)
553
+
554
+
555
+ @schedules_app.command("replace")
556
+ def replace_schedule(
557
+ schedule_rid: str = typer.Argument(
558
+ ..., help="Schedule Resource Identifier", autocompletion=complete_rid
559
+ ),
560
+ action: str = typer.Argument(..., help="Schedule action (JSON format)"),
561
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
562
+ display_name: Optional[str] = typer.Option(
563
+ None, "--name", help="Display name for the schedule"
564
+ ),
565
+ description: Optional[str] = typer.Option(
566
+ None, "--description", help="Schedule description"
567
+ ),
568
+ trigger: Optional[str] = typer.Option(
569
+ None, "--trigger", help="Trigger configuration (JSON format)"
570
+ ),
571
+ preview: bool = typer.Option(False, "--preview", help="Enable preview mode"),
572
+ format: str = typer.Option(
573
+ "table", "--format", "-f", help="Output format (table, json, csv)"
574
+ ),
575
+ ):
576
+ """Replace an existing schedule."""
577
+ try:
578
+ service = OrchestrationService(profile=profile)
579
+
580
+ # Parse action JSON
581
+ try:
582
+ action_dict = json.loads(action)
583
+ except json.JSONDecodeError:
584
+ formatter.print_error("Invalid JSON format for action")
585
+ raise typer.Exit(1)
586
+
587
+ # Parse trigger JSON if provided
588
+ trigger_dict = None
589
+ if trigger:
590
+ try:
591
+ trigger_dict = json.loads(trigger)
592
+ except json.JSONDecodeError:
593
+ formatter.print_error("Invalid JSON format for trigger")
594
+ raise typer.Exit(1)
595
+
596
+ with SpinnerProgressTracker().track_spinner(
597
+ f"Replacing schedule {schedule_rid}..."
598
+ ):
599
+ schedule = service.replace_schedule(
600
+ schedule_rid=schedule_rid,
601
+ action=action_dict,
602
+ display_name=display_name,
603
+ description=description,
604
+ trigger=trigger_dict,
605
+ preview=preview,
606
+ )
607
+
608
+ formatter.print_success(f"Successfully replaced schedule {schedule_rid}")
609
+ formatter.format_schedule_detail(schedule, format)
610
+
611
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
612
+ formatter.print_error(f"Authentication error: {e}")
613
+ raise typer.Exit(1)
614
+ except Exception as e:
615
+ formatter.print_error(f"Failed to replace schedule: {e}")
616
+ raise typer.Exit(1)
617
+
618
+
619
+ # ============================================================================
620
+ # Main app setup
621
+ # ============================================================================
622
+
623
+ # Add sub-apps to main app
624
+ app.add_typer(builds_app, name="builds", help="Manage builds")
625
+ app.add_typer(jobs_app, name="jobs", help="Manage jobs")
626
+ app.add_typer(schedules_app, name="schedules", help="Manage schedules")
627
+
628
+
629
+ @app.callback()
630
+ def main():
631
+ """
632
+ Orchestration operations for managing builds, jobs, and schedules.
633
+
634
+ This module provides commands to:
635
+ - Create, monitor, and cancel builds
636
+ - View job details and statuses
637
+ - Create, manage, and execute schedules
638
+
639
+ All operations require Resource Identifiers (RIDs) like:
640
+ ri.orchestration.main.build.12345678-1234-1234-1234-123456789abc
641
+ """
642
+ pass