lumos-identity-cli 0.0.0.dev1__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.
lumos/request/cli.py ADDED
@@ -0,0 +1,523 @@
1
+ import re
2
+ import time
3
+ from uuid import UUID
4
+
5
+ from click_extra import Context, confirm, echo, group, option, pass_context, prompt
6
+ from pick import pick
7
+ from tabulate import tabulate
8
+
9
+ from lumos.common.client import ApiClient
10
+ from lumos.common.helpers import authenticate
11
+ from lumos.common.models import (
12
+ AccessRequest,
13
+ App,
14
+ Permission,
15
+ ProvisioningMethodOption,
16
+ SupportRequestStatus,
17
+ User,
18
+ )
19
+
20
+ POLLING_INTERVAL = 6
21
+ client = ApiClient()
22
+
23
+
24
+ @group(name="request", invoke_without_command=True)
25
+ @pass_context
26
+ @option("--reason", default=None, help="Business justification for request")
27
+ @option(
28
+ "--for-user",
29
+ default=None,
30
+ type=str,
31
+ help="UUID of user for whom to request access. Takes precedence over --user-like",
32
+ )
33
+ @option("--for-me", is_flag=True, help="Makes the request for the current user.")
34
+ @option(
35
+ "--mine",
36
+ is_flag=True,
37
+ help="Makes the request for the current user. Duplicate of --for-me for convenience.",
38
+ )
39
+ @option("--app", default=None, type=str, help="App UUID. Takes precedence over --app-like")
40
+ @option(
41
+ "--permission",
42
+ multiple=True,
43
+ type=str,
44
+ help="List of permission UUIDs. Takes precedence over --permission-like",
45
+ )
46
+ @option(
47
+ "--length",
48
+ default=None,
49
+ help="Length of access request in seconds, or a string like '12 hours', '2d', 'unlimited', etc. Every app/permission has different configurations.",
50
+ )
51
+ @option(
52
+ "--user-like",
53
+ default=None,
54
+ help="User name/email like--filters users shown as options when selecting, if the request is not for the current user",
55
+ )
56
+ @option(
57
+ "--app-like",
58
+ default=None,
59
+ help="App name like--filters apps shown as options when selecting",
60
+ )
61
+ @option(
62
+ "--permission-like",
63
+ default=None,
64
+ help="Permission name like--filters permissions shown as options when selecting",
65
+ )
66
+ @option("--wait/--no-wait", default=None, help="Wait for the request to complete")
67
+ @option(
68
+ "--dry-run",
69
+ is_flag=True,
70
+ help="Print the request command without actually making the request",
71
+ )
72
+ @authenticate
73
+ def request(
74
+ ctx: Context,
75
+ reason: str | None,
76
+ for_user: str | None,
77
+ for_me: bool,
78
+ mine: bool,
79
+ app: str | None,
80
+ permission: tuple,
81
+ length: str | None,
82
+ user_like: str | None,
83
+ app_like: str | None,
84
+ permission_like: str | None,
85
+ wait: bool | None,
86
+ dry_run: bool,
87
+ ) -> None:
88
+ """Request access to an app."""
89
+ if ctx.invoked_subcommand is None:
90
+ for_user_uuid = None
91
+ if for_me is not True and mine is not True:
92
+ if user_like is not None or not confirm("This request is for you?", default=True):
93
+ for_user_uuid = select_user(user_like)
94
+ elif for_user:
95
+ for_user_uuid = UUID(for_user)
96
+
97
+ # Validate parameters or interactively input them
98
+ app_uuid = UUID(app) if app else None
99
+ selected_app: App = get_valid_app(app_uuid, app_like)
100
+ app_settings = client.get_appstore_app_setting(selected_app.id)
101
+ duration_options = set(app_settings.provisioning.time_based_access)
102
+ selected_permissions = None
103
+
104
+ permission_uuids = [UUID(p) for p in permission] if permission else None
105
+
106
+ if app_settings.provisioning.groups_provisioning == ProvisioningMethodOption.GROUPS_AND_VISIBLE:
107
+ selected_permissions = select_permissions(
108
+ selected_app,
109
+ app_settings.provisioning.allow_multiple_permission_selection,
110
+ permission_uuids,
111
+ permission_like,
112
+ )
113
+ if selected_permissions:
114
+ duration_options = set(selected_permissions[0].duration_options)
115
+ for perm in selected_permissions[1:]:
116
+ duration_options = duration_options.intersection(perm.duration_options)
117
+
118
+ duration, duration_friendly = get_duration(duration_options, length)
119
+
120
+ while not reason or len(reason) < 1:
121
+ reason = prompt("\nEnter your business justification for the request")
122
+
123
+ if wait is None:
124
+ wait = confirm("Do you want to wait for the request to complete?", default=True)
125
+
126
+ echo("\nAPP")
127
+ echo(f" {selected_app.user_friendly_label} [{selected_app.id}]")
128
+ if selected_permissions:
129
+ echo("\nPERMISSIONS")
130
+ for perm in selected_permissions:
131
+ echo(f" {perm.label} [{perm.id}]")
132
+ echo("\nDURATION")
133
+ echo(f" {duration_friendly or 'Unlimited'} {f'({duration} seconds)' if duration else ''}")
134
+ echo("\nREASON")
135
+ echo(f" {reason}")
136
+
137
+ if for_user_uuid:
138
+ echo("\nTARGET USER")
139
+ echo(f" {for_user_uuid}")
140
+
141
+ permission_flags = ""
142
+ if selected_permissions:
143
+ permission_flags = " ".join([f"--permission {perm.id}" for perm in selected_permissions])
144
+
145
+ command_str = f'lumos request --app {selected_app.id} {permission_flags} --reason "{reason}"'
146
+ if duration:
147
+ command_str += f" --length {duration}"
148
+ for_user_flag = "--for-me"
149
+ if for_user_uuid:
150
+ for_user_flag = "--for-user USER_ID"
151
+ command_str += f" {for_user_flag}"
152
+ if wait:
153
+ command_str += " --wait"
154
+ else:
155
+ command_str += " --no-wait"
156
+ if dry_run:
157
+ echo("\nCOMMAND")
158
+ echo(f" {command_str}")
159
+ return
160
+
161
+ echo("\nIf you need to make this same request in the future, use:")
162
+ echo(f"\n `{command_str}`\n")
163
+
164
+ request_id = create_access_request(
165
+ app_id=selected_app.id,
166
+ requestable_permission_ids=([p.id for p in selected_permissions] if selected_permissions else None),
167
+ note=reason,
168
+ expiration=duration,
169
+ target_user_id=for_user_uuid,
170
+ )
171
+
172
+ if wait and request_id:
173
+ _poll(request_id)
174
+
175
+
176
+ @request.command(
177
+ "status",
178
+ help="Check the status of a request by ID or `--last` for the most recent request",
179
+ )
180
+ @option("--request-id", default=None, help="Request ID")
181
+ @option("--last", is_flag=True, help="Get the last request")
182
+ @option("--status-only", is_flag=True, help="Output status only")
183
+ @option("--permission-only", is_flag=True, help="Output permission only")
184
+ @option("--id-only", is_flag=True, help="Output request ID only")
185
+ @authenticate
186
+ def status(
187
+ request_id: str | None,
188
+ last: bool,
189
+ status_only: bool,
190
+ permission_only: bool,
191
+ id_only: bool,
192
+ ) -> None:
193
+ access_request: AccessRequest | None
194
+ if last:
195
+ access_requests, count, _, _, _ = client.get_access_requests(target_user_id=client.get_current_user_id())
196
+ if count == 0:
197
+ echo("No pending requests found")
198
+ return
199
+ access_request = access_requests[0]
200
+ else:
201
+ request_uuid: UUID | None = None
202
+ while not request_uuid:
203
+ try:
204
+ request_uuid = UUID(request_id)
205
+ break
206
+ except ValueError:
207
+ if request_id:
208
+ echo("Invalid request ID")
209
+ request_id = None
210
+ request_id = prompt("Please provide a request ID")
211
+
212
+ access_request = client.get_request_status(request_uuid)
213
+ if not access_request:
214
+ echo("Request not found")
215
+ return
216
+ if status_only:
217
+ echo(access_request.status)
218
+ return
219
+ if permission_only:
220
+ if access_request.requestable_permissions:
221
+ for perm in access_request.requestable_permissions:
222
+ echo(perm.label)
223
+ return
224
+ if last and id_only:
225
+ echo(access_request.status)
226
+ return
227
+ print(tabulate([access_request.tabulate()], headers=AccessRequest.headers()), "\n")
228
+
229
+
230
+ @request.command("poll", help="Poll a request by ID for up to 5 minutes")
231
+ @option("--request-id", default=None, type=str, help="Request ID")
232
+ @option("--wait", default=2, help="How many minutes to wait. Max 5.")
233
+ @authenticate
234
+ def poll(request_id: str | None, wait: int) -> None:
235
+ request_uuid = UUID(request_id) if request_id else None
236
+ while not request_uuid:
237
+ request_id_str = prompt("Please provide a request ID")
238
+ request_uuid = UUID(request_id_str)
239
+
240
+ _poll(request_uuid, (wait or 2) * 60)
241
+
242
+
243
+ def _poll(request_id: UUID, wait_max: int = 120):
244
+ if wait_max < 10 or wait_max > 300:
245
+ wait_max = 120
246
+ while (access_request := client.get_request_status(request_id)) is not None:
247
+ if access_request.status not in SupportRequestStatus.PENDING_STATUSES or wait_max <= 0:
248
+ break
249
+ wait_max -= POLLING_INTERVAL
250
+ for num_decimals in range(POLLING_INTERVAL):
251
+ time.sleep(1)
252
+ print(
253
+ " ⏰ Waiting for request to complete"
254
+ + ("." * num_decimals)
255
+ + (" " * (POLLING_INTERVAL - num_decimals)),
256
+ end="\r",
257
+ )
258
+
259
+ if not access_request:
260
+ echo("Request not found")
261
+ raise SystemExit(1)
262
+
263
+ if access_request.status == SupportRequestStatus.COMPLETED:
264
+ echo(" ✅ Request completed!")
265
+ return
266
+ echo(f" ⏰ Request status: {access_request.status}" + (" " * 20) + "\n")
267
+ echo(f"Use `lumos request status --request-id {request_id}` to check the status later.")
268
+
269
+
270
+ @request.command("cancel", help="Cancel a request by ID")
271
+ @option("--request-id", default=None, type=str, help="Request ID")
272
+ @option("--reason", default=None, help="Reason for cancellation")
273
+ @authenticate
274
+ def cancel(request_id: str | None, reason: str | None) -> None:
275
+ request_uuid = None
276
+ while not request_uuid:
277
+ if not request_id:
278
+ request_id = prompt(
279
+ "Please provide a request ID or press enter to look up a request",
280
+ default="",
281
+ show_default=False,
282
+ )
283
+ if not request_id:
284
+ pending_requests, _, _, _, _ = client.get_access_requests(
285
+ client.get_current_user_id(),
286
+ SupportRequestStatus.PENDING_STATUSES,
287
+ all=True,
288
+ )
289
+ if len(pending_requests) == 0:
290
+ echo("No pending requests found")
291
+ raise SystemExit(1)
292
+ access_request, _ = pick(pending_requests, "Select a request to cancel")
293
+ request_uuid = access_request.id
294
+ else:
295
+ request_uuid = UUID(request_id)
296
+
297
+ if (
298
+ access_request := client.get_request_status(request_uuid)
299
+ ) is not None and access_request.status not in SupportRequestStatus.PENDING_STATUSES:
300
+ echo(f"Request is not pending, cannot cancel. Status is {access_request.status}.")
301
+ raise SystemExit(1)
302
+
303
+ client.cancel_access_request(request_uuid, reason)
304
+ echo("Request cancelled! 🚫")
305
+
306
+
307
+ def select_user(user_like: str | None = None) -> UUID:
308
+ users: list[User] = []
309
+ for_user: User | None = None
310
+ while for_user is None:
311
+ while True:
312
+ users, count, total = client.get_users(like=user_like)
313
+ if total == 0:
314
+ if user_like:
315
+ echo(f"No users found for '{user_like}'")
316
+ user_like = prompt("🔍 Give me something to search on")
317
+ else:
318
+ echo("No users found")
319
+ raise SystemExit(1)
320
+ elif count == 1:
321
+ for_user = users[0]
322
+ break
323
+ elif count < total:
324
+ echo("Too many users to show.")
325
+ user_like = prompt("🔍 Give me something to search on")
326
+ else:
327
+ break
328
+ if not for_user:
329
+ description = "Select user (use ENTER to confirm)"
330
+ for_user, _ = pick(users, description)
331
+ echo(f"USER: {for_user.email} [{for_user.id}]\n")
332
+ return for_user.id
333
+
334
+
335
+ def get_valid_app(app_id: UUID | None = None, app_like: str | None = None) -> App:
336
+ app = None
337
+ while not app_id or not (app := client.get_appstore_app(app_id)):
338
+ apps: list[App] = []
339
+ while True:
340
+ print("\n⏳ Loading your apps ...", end="\r")
341
+ apps, count, total = client.get_appstore_apps(name_search=app_like, page_size=25)
342
+ print(" ", end="\r")
343
+ if total == 0:
344
+ if app_like:
345
+ app_like = prompt(f"No apps found for '{app_like}'\n🔍 Give me something to search on")
346
+ else:
347
+ echo("No apps found")
348
+ raise SystemExit(1)
349
+ elif count < total:
350
+ echo("")
351
+ app_like = prompt("Too many apps to show.\n🔍 Give me something to search on")
352
+ else:
353
+ break
354
+ if count == 1:
355
+ app = apps[0]
356
+ else:
357
+ app, _ = pick(apps, "Select an app (press ENTER to confirm)")
358
+ if app:
359
+ app_id = app.id
360
+ echo(f"APP: {app.user_friendly_label} [{app.id}]\n")
361
+ return app
362
+
363
+
364
+ def select_permissions(
365
+ app: App,
366
+ allow_multiple_permission_selection: bool,
367
+ permission_ids: list[UUID] | None,
368
+ permission_like: str | None = None,
369
+ ) -> list[Permission] | None:
370
+ valid_permissions = get_valid_permissions(app, permission_ids, allow_multiple_permission_selection)
371
+ if len(valid_permissions) > 0:
372
+ return valid_permissions
373
+ done_selecting = False
374
+ valid_permissions_dict: dict[str, Permission] = {}
375
+ while not done_selecting:
376
+ permissions: list[Permission] = []
377
+ while True:
378
+ print("\n⏳ Loading permissions for app ...", end="\r")
379
+ permissions, count, total = client.get_app_requestable_permissions(
380
+ app_id=app.id, search_term=permission_like, page_size=25
381
+ )
382
+ print(" ", end="\r")
383
+ if total == 0:
384
+ if permission_like:
385
+ permission_like = prompt(
386
+ f"No permissions found for '{permission_like}'\n🔍 Give me something to search on"
387
+ )
388
+ else:
389
+ echo("No permissions found (you're just requesting the app)")
390
+ return None
391
+ elif count < total:
392
+ permission_like = prompt("Too many permissions to show.\n🔍 Give me something to search on")
393
+ else:
394
+ break
395
+ if count > 1:
396
+ already_selected = ", ".join([p.label for p in valid_permissions_dict.values()])
397
+ if allow_multiple_permission_selection:
398
+ description = f"Select at least one permissions\n{f'(already selected: {already_selected})' if already_selected else ''}\nUse SPACE or right arrow to select, ENTER to confirm"
399
+ selected = pick(permissions, description, multiselect=True, min_selection_count=1) # type: ignore[misc]
400
+ for option, _ in selected:
401
+ valid_permissions_dict[str(option.id)] = option
402
+ else:
403
+ option, _ = pick(permissions, "Select permission (use ENTER to confirm)")
404
+ valid_permissions_dict[str(option.id)] = option
405
+ elif count == 1:
406
+ permission = permissions[0]
407
+ valid_permissions_dict[str(permission.id)] = permission
408
+ permission_like = None
409
+ echo("PERMISSIONS: ")
410
+ for permission in valid_permissions_dict.values():
411
+ echo(f" {permission.label} [{permission.id}]")
412
+ if not permission_like or not allow_multiple_permission_selection:
413
+ break
414
+ if confirm("Done selecting permissions?", default=True):
415
+ done_selecting = True
416
+ else:
417
+ permission_like = prompt("🔍 Give me something to search on")
418
+ return list(valid_permissions_dict.values())
419
+
420
+
421
+ def get_valid_permissions(
422
+ app: App,
423
+ permission_ids: list[UUID] | None,
424
+ allow_multiple_permission_selection: bool,
425
+ ) -> list[Permission]:
426
+ valid_permissions: list[Permission] = []
427
+ if not permission_ids:
428
+ return []
429
+ if len(permission_ids) > 1 and not allow_multiple_permission_selection:
430
+ return []
431
+ for permission_id in permission_ids:
432
+ if not (permission := client.get_app_requestable_permission(permission_id)):
433
+ return []
434
+ if not permission.app_id.__eq__(app.id):
435
+ return []
436
+ valid_permissions.append(permission)
437
+ return valid_permissions
438
+
439
+
440
+ def select_duration(durations: set[str], duration_friendly: str | None) -> tuple[int | None, str]:
441
+ selected: str = ""
442
+ if len(durations) == 1:
443
+ selected = next(iter(durations))
444
+ elif duration_friendly in durations:
445
+ selected = duration_friendly
446
+ elif len(durations) > 1:
447
+ selected, _ = pick(
448
+ list(durations),
449
+ "Select duration (use ENTER to confirm)",
450
+ multiselect=False,
451
+ min_selection_count=1,
452
+ )
453
+
454
+ time_in_seconds = None
455
+ if match := re.match(r"(\d+) ", selected):
456
+ time_in_seconds = int(match.group(1)) * 60 * 60
457
+ if re.match(r".*days", selected):
458
+ time_in_seconds = 24 * time_in_seconds
459
+ echo(f"DURATION: {selected}{f' ({time_in_seconds} seconds)' if time_in_seconds else ''}")
460
+ return time_in_seconds, selected
461
+
462
+
463
+ def parse_duration(duration: str) -> tuple[int | None, str]:
464
+ time_in_seconds = None
465
+ if match := re.match(r"(\d+) ", duration):
466
+ time_in_seconds = int(match.group(1)) * 60 * 60
467
+ if re.match(r".* d", duration):
468
+ time_in_seconds = 24 * time_in_seconds
469
+ return time_in_seconds, duration.replace(" ", "").lower()
470
+
471
+
472
+ def get_duration(possible_durations: set[str], input_length: str | None) -> tuple[int | None, str]:
473
+ duration_friendly: str | None = None
474
+ durations = {}
475
+ for possible_duration in possible_durations:
476
+ length, key = parse_duration(possible_duration)
477
+
478
+ durations[key] = {
479
+ "label": possible_duration,
480
+ "length": length,
481
+ }
482
+ if input_length:
483
+ try:
484
+ duration = int(input_length)
485
+ duration_friendly = None
486
+ for _key, v in durations.items():
487
+ if v["length"] == duration:
488
+ duration_friendly = v["label"]
489
+ break
490
+ except ValueError:
491
+ for key, v in durations.items():
492
+ if key.startswith(input_length.replace(" ", "").lower()):
493
+ duration_friendly = v["label"]
494
+ break
495
+
496
+ duration, duration_friendly = select_duration(possible_durations, duration_friendly)
497
+ return duration, duration_friendly
498
+
499
+
500
+ def create_access_request(
501
+ app_id: UUID,
502
+ requestable_permission_ids: list[UUID] | None,
503
+ note: str,
504
+ expiration: int | None = None,
505
+ target_user_id: UUID | None = None,
506
+ ) -> UUID | None:
507
+ response = client.create_access_request(
508
+ app_id=app_id,
509
+ permission_ids=requestable_permission_ids,
510
+ note=note,
511
+ expiration_in_seconds=expiration,
512
+ target_user_id=target_user_id,
513
+ )
514
+ if not response:
515
+ return None
516
+
517
+ echo(f"\nYour request (ID {response.id}) is in progress! 🏃🌴\n")
518
+
519
+ return response.id
520
+
521
+
522
+ if __name__ == "__main__":
523
+ request()
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumos-identity-cli
3
+ Version: 0.0.0.dev1
4
+ Summary: Lumos command line interface.
5
+ Project-URL: Homepage, https://github.com/teamlumos/lumos-cli
6
+ Project-URL: Repository, https://github.com/teamlumos/lumos-cli
7
+ Project-URL: Issues, https://github.com/teamlumos/lumos-cli/issues
8
+ Author: lumos-dev
9
+ License: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: <4,>=3.10.6
17
+ Requires-Dist: click-extra<8,>=7.4.0
18
+ Requires-Dist: pick<3,>=2.2.0
19
+ Requires-Dist: pydantic<3,>=2.6.0
20
+ Requires-Dist: pytz>=2024.1
21
+ Requires-Dist: requests<3,>=2.31.0
22
+ Requires-Dist: tabulate<1,>=0.9.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Lumos CLI
26
+
27
+ A command-line interface for the Lumos platform. Manage access requests, list resources, and automate workflows.
28
+
29
+ ## Installation
30
+
31
+ ### Homebrew (macOS and Linux)
32
+
33
+ ```shell
34
+ brew install teamlumos/tap/lumos
35
+ ```
36
+
37
+ ### PyPI (Python environments)
38
+
39
+ ```shell
40
+ pip install lumos-identity-cli
41
+ ```
42
+
43
+ ### Other Methods
44
+
45
+ See [Installation Guide](docs/installation.md) for pip, uv, and binary installation options.
46
+
47
+ ## Quick Start
48
+
49
+ ```shell
50
+ # Setup authentication
51
+ lumos setup
52
+
53
+ # Check current user
54
+ lumos whoami
55
+
56
+ # List available apps
57
+ lumos list apps --like github
58
+
59
+ # Request access
60
+ lumos request
61
+ ```
62
+
63
+ ## Documentation
64
+
65
+ - **[Installation](docs/installation.md)** - Detailed installation instructions for all platforms
66
+ - **[Examples](docs/examples.md)** - Usage examples with code samples
67
+ - **[CLI Reference](docs/reference.md)** - Complete command reference
68
+
69
+ Online documentation: [teamlumos.github.io/lumos-cli](https://teamlumos.github.io/lumos-cli/)
70
+
71
+ ## Development
72
+
73
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,18 @@
1
+ lumos/__init__.py,sha256=EUuV3XMjE8eHwrdlQ3wm0sTS-t9FjmqYvHThOlIaQQw,113
2
+ lumos/__main__.py,sha256=ZoCjA4DBKJmVL6eNeaWMjeoxWH4f7V6DcroWJIIt4bw,94
3
+ lumos/cli.py,sha256=CgxOotePGsrZKTSGyGfz-O8nNPnWkVOc4Mwdn642sv0,2748
4
+ lumos/common/__init__.py,sha256=Ns0NGKHhAuiv4_SYqbIQn1-C8mRvRdaGrHAHUo99USE,33
5
+ lumos/common/client.py,sha256=zk-hSTf7gJYaBFfPu_y3HwbqhlRUrI8kF5YI03y0htU,16834
6
+ lumos/common/client_helpers.py,sha256=n9PF2wc25-sfZd7dLgcZFBlls3DrFAWhYUbEYXq68NY,1253
7
+ lumos/common/helpers.py,sha256=pv_OAQXRWz2gWNvjG_Sr609yCSz7o803tAubyH9kopQ,3004
8
+ lumos/common/keyhelpers.py,sha256=bAxiuHDvUf2GB3eEfBSlZsiwSL0RNKOZ5NSKeVkFNVY,955
9
+ lumos/common/logging.py,sha256=A4617xvoM5sJg0H6vC6r6NWNcr0ftmFKIY2JqI69-fg,937
10
+ lumos/common/models.py,sha256=llyWPG7QYoGn12u9-zDiAJPGUsmqN9QSniXY7aDknPo,7252
11
+ lumos/list_collections/__init__.py,sha256=so_qquyp3GDq_6VgQ58hb1ka7kttpvERnvuaH0jEKBo,42
12
+ lumos/list_collections/cli.py,sha256=8gDbyiLQfQvGfq3e6tTd3dSiXznIid9OKTf40shY4Dk,8246
13
+ lumos/request/__init__.py,sha256=5oDglMtFk9kr4BS-jyEn7yeXrqneWktoel0S5zmdWyM,36
14
+ lumos/request/cli.py,sha256=tg3ZO0Tq0vR-htEA_QFrR3JjCSg58h5CRj88xL9FpHw,19168
15
+ lumos_identity_cli-0.0.0.dev1.dist-info/METADATA,sha256=U9mACEoH9xezYzyBnB21E-89jvo8h2cjfevPdgTuVsw,1957
16
+ lumos_identity_cli-0.0.0.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ lumos_identity_cli-0.0.0.dev1.dist-info/entry_points.txt,sha256=CiQ7iTJBH8Ay4RAZ9if8t_AJtY3hYei7rKToz3Rr-74,42
18
+ lumos_identity_cli-0.0.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lumos = lumos.cli:lumos