pygeai 0.6.0b11__py3-none-any.whl → 0.6.0b12__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.
Files changed (132) hide show
  1. pygeai/_docs/source/content/ai_lab/cli.rst +4 -4
  2. pygeai/_docs/source/content/ai_lab/models.rst +169 -35
  3. pygeai/_docs/source/content/ai_lab/runner.rst +2 -2
  4. pygeai/_docs/source/content/ai_lab/spec.rst +9 -9
  5. pygeai/_docs/source/content/ai_lab/usage.rst +34 -34
  6. pygeai/_docs/source/content/ai_lab.rst +1 -1
  7. pygeai/_docs/source/content/analytics.rst +598 -0
  8. pygeai/_docs/source/content/api_reference/chat.rst +428 -2
  9. pygeai/_docs/source/content/api_reference/embeddings.rst +1 -1
  10. pygeai/_docs/source/content/api_reference/project.rst +184 -0
  11. pygeai/_docs/source/content/api_reference/rag.rst +2 -2
  12. pygeai/_docs/source/content/authentication.rst +295 -0
  13. pygeai/_docs/source/content/cli.rst +79 -2
  14. pygeai/_docs/source/content/debugger.rst +1 -1
  15. pygeai/_docs/source/content/migration.rst +19 -2
  16. pygeai/_docs/source/index.rst +2 -0
  17. pygeai/_docs/source/pygeai.analytics.rst +53 -0
  18. pygeai/_docs/source/pygeai.cli.commands.rst +8 -0
  19. pygeai/_docs/source/pygeai.rst +1 -0
  20. pygeai/_docs/source/pygeai.tests.analytics.rst +45 -0
  21. pygeai/_docs/source/pygeai.tests.auth.rst +8 -0
  22. pygeai/_docs/source/pygeai.tests.rst +1 -1
  23. pygeai/analytics/__init__.py +0 -0
  24. pygeai/analytics/clients.py +505 -0
  25. pygeai/analytics/endpoints.py +35 -0
  26. pygeai/analytics/managers.py +606 -0
  27. pygeai/analytics/mappers.py +207 -0
  28. pygeai/analytics/responses.py +240 -0
  29. pygeai/cli/commands/analytics.py +525 -0
  30. pygeai/cli/commands/base.py +16 -0
  31. pygeai/cli/commands/common.py +28 -24
  32. pygeai/cli/commands/migrate.py +75 -6
  33. pygeai/cli/commands/organization.py +265 -0
  34. pygeai/cli/commands/validators.py +144 -1
  35. pygeai/cli/error_handler.py +41 -6
  36. pygeai/cli/geai.py +99 -16
  37. pygeai/cli/parsers.py +75 -31
  38. pygeai/cli/texts/help.py +75 -6
  39. pygeai/core/base/clients.py +18 -4
  40. pygeai/core/base/session.py +46 -7
  41. pygeai/core/common/config.py +25 -2
  42. pygeai/core/common/exceptions.py +64 -1
  43. pygeai/core/services/rest.py +20 -2
  44. pygeai/evaluation/clients.py +5 -3
  45. pygeai/lab/agents/clients.py +3 -3
  46. pygeai/lab/agents/endpoints.py +2 -2
  47. pygeai/lab/agents/mappers.py +50 -2
  48. pygeai/lab/clients.py +5 -2
  49. pygeai/lab/managers.py +7 -9
  50. pygeai/lab/models.py +70 -2
  51. pygeai/lab/tools/clients.py +1 -59
  52. pygeai/migration/__init__.py +3 -1
  53. pygeai/migration/strategies.py +72 -3
  54. pygeai/organization/clients.py +110 -1
  55. pygeai/organization/endpoints.py +11 -7
  56. pygeai/organization/managers.py +134 -2
  57. pygeai/organization/mappers.py +28 -2
  58. pygeai/organization/responses.py +11 -1
  59. pygeai/tests/analytics/__init__.py +0 -0
  60. pygeai/tests/analytics/test_clients.py +86 -0
  61. pygeai/tests/analytics/test_managers.py +94 -0
  62. pygeai/tests/analytics/test_mappers.py +84 -0
  63. pygeai/tests/analytics/test_responses.py +73 -0
  64. pygeai/tests/auth/test_oauth.py +172 -0
  65. pygeai/tests/cli/commands/test_migrate.py +14 -1
  66. pygeai/tests/cli/commands/test_organization.py +69 -1
  67. pygeai/tests/cli/test_error_handler.py +4 -4
  68. pygeai/tests/cli/test_geai_driver.py +1 -1
  69. pygeai/tests/lab/agents/test_mappers.py +128 -1
  70. pygeai/tests/lab/test_models.py +2 -0
  71. pygeai/tests/lab/tools/test_clients.py +2 -31
  72. pygeai/tests/organization/test_clients.py +180 -1
  73. pygeai/tests/organization/test_managers.py +40 -0
  74. pygeai/tests/snippets/analytics/__init__.py +0 -0
  75. pygeai/tests/snippets/analytics/get_agent_usage_per_user.py +16 -0
  76. pygeai/tests/snippets/analytics/get_agents_created_and_modified.py +11 -0
  77. pygeai/tests/snippets/analytics/get_average_cost_per_request.py +10 -0
  78. pygeai/tests/snippets/analytics/get_overall_error_rate.py +10 -0
  79. pygeai/tests/snippets/analytics/get_top_10_agents_by_requests.py +12 -0
  80. pygeai/tests/snippets/analytics/get_total_active_users.py +10 -0
  81. pygeai/tests/snippets/analytics/get_total_cost.py +10 -0
  82. pygeai/tests/snippets/analytics/get_total_requests_per_day.py +12 -0
  83. pygeai/tests/snippets/analytics/get_total_tokens.py +12 -0
  84. pygeai/tests/snippets/chat/get_response_complete_example.py +67 -0
  85. pygeai/tests/snippets/chat/get_response_with_instructions.py +19 -0
  86. pygeai/tests/snippets/chat/get_response_with_metadata.py +24 -0
  87. pygeai/tests/snippets/chat/get_response_with_parallel_tools.py +58 -0
  88. pygeai/tests/snippets/chat/get_response_with_reasoning.py +21 -0
  89. pygeai/tests/snippets/chat/get_response_with_store.py +38 -0
  90. pygeai/tests/snippets/chat/get_response_with_truncation.py +24 -0
  91. pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +39 -0
  92. pygeai/tests/snippets/lab/agents/create_agent_with_properties.py +46 -0
  93. pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +62 -0
  94. pygeai/tests/snippets/lab/agents/update_agent_properties.py +50 -0
  95. pygeai/tests/snippets/organization/add_project_member.py +10 -0
  96. pygeai/tests/snippets/organization/add_project_member_batch.py +44 -0
  97. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/METADATA +1 -1
  98. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/RECORD +102 -92
  99. pygeai/_docs/source/pygeai.tests.snippets.assistants.data_analyst.rst +0 -37
  100. pygeai/_docs/source/pygeai.tests.snippets.assistants.rag.rst +0 -85
  101. pygeai/_docs/source/pygeai.tests.snippets.assistants.rst +0 -78
  102. pygeai/_docs/source/pygeai.tests.snippets.auth.rst +0 -10
  103. pygeai/_docs/source/pygeai.tests.snippets.chat.rst +0 -125
  104. pygeai/_docs/source/pygeai.tests.snippets.dbg.rst +0 -45
  105. pygeai/_docs/source/pygeai.tests.snippets.embeddings.rst +0 -61
  106. pygeai/_docs/source/pygeai.tests.snippets.evaluation.dataset.rst +0 -197
  107. pygeai/_docs/source/pygeai.tests.snippets.evaluation.plan.rst +0 -133
  108. pygeai/_docs/source/pygeai.tests.snippets.evaluation.result.rst +0 -37
  109. pygeai/_docs/source/pygeai.tests.snippets.evaluation.rst +0 -20
  110. pygeai/_docs/source/pygeai.tests.snippets.extras.rst +0 -37
  111. pygeai/_docs/source/pygeai.tests.snippets.files.rst +0 -53
  112. pygeai/_docs/source/pygeai.tests.snippets.gam.rst +0 -21
  113. pygeai/_docs/source/pygeai.tests.snippets.lab.agents.rst +0 -93
  114. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.jobs.rst +0 -21
  115. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.kbs.rst +0 -45
  116. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.rst +0 -46
  117. pygeai/_docs/source/pygeai.tests.snippets.lab.rst +0 -82
  118. pygeai/_docs/source/pygeai.tests.snippets.lab.samples.rst +0 -21
  119. pygeai/_docs/source/pygeai.tests.snippets.lab.strategies.rst +0 -45
  120. pygeai/_docs/source/pygeai.tests.snippets.lab.tools.rst +0 -85
  121. pygeai/_docs/source/pygeai.tests.snippets.lab.use_cases.rst +0 -117
  122. pygeai/_docs/source/pygeai.tests.snippets.migrate.rst +0 -10
  123. pygeai/_docs/source/pygeai.tests.snippets.organization.rst +0 -109
  124. pygeai/_docs/source/pygeai.tests.snippets.rag.rst +0 -85
  125. pygeai/_docs/source/pygeai.tests.snippets.rerank.rst +0 -21
  126. pygeai/_docs/source/pygeai.tests.snippets.rst +0 -32
  127. pygeai/_docs/source/pygeai.tests.snippets.secrets.rst +0 -10
  128. pygeai/_docs/source/pygeai.tests.snippets.usage_limit.rst +0 -77
  129. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/WHEEL +0 -0
  130. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/entry_points.txt +0 -0
  131. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/licenses/LICENSE +0 -0
  132. {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,7 @@ from pygeai.assistant.managers import AssistantManager
11
11
  from pygeai.assistant.rag.clients import RAGAssistantClient
12
12
  from pygeai.assistant.rag.mappers import RAGAssistantMapper
13
13
  from pygeai.core.files.managers import FileManager
14
+ from pygeai.core.secrets.clients import SecretClient
14
15
  from pygeai.migration.strategies import (
15
16
  ProjectMigrationStrategy,
16
17
  AgentMigrationStrategy,
@@ -19,7 +20,8 @@ from pygeai.migration.strategies import (
19
20
  TaskMigrationStrategy,
20
21
  UsageLimitMigrationStrategy,
21
22
  RAGAssistantMigrationStrategy,
22
- FileMigrationStrategy
23
+ FileMigrationStrategy,
24
+ SecretMigrationStrategy
23
25
  )
24
26
  from pygeai.migration.tools import MigrationTool, MigrationPlan, MigrationOrchestrator
25
27
  from pygeai.admin.clients import AdminClient
@@ -216,7 +218,7 @@ def select_resource_types() -> List[int]:
216
218
  """
217
219
  Prompt user to select which resource types to migrate.
218
220
 
219
- :return: List of integers representing selected resource types (1-7)
221
+ :return: List of integers representing selected resource types (1-8)
220
222
  """
221
223
  Console.write_stdout("\n--- Resource Type Selection ---")
222
224
  Console.write_stdout("Which resource types do you want to migrate?")
@@ -227,15 +229,16 @@ def select_resource_types() -> List[int]:
227
229
  Console.write_stdout(" 5. RAG Assistants")
228
230
  Console.write_stdout(" 6. Files")
229
231
  Console.write_stdout(" 7. Usage Limits")
232
+ Console.write_stdout(" 8. Secrets")
230
233
 
231
234
  while True:
232
235
  resource_choice = input("\nSelect resource types (comma-separated numbers, or empty for all): ").strip()
233
236
  if not resource_choice:
234
- return [1, 2, 3, 4, 5, 6, 7]
237
+ return [1, 2, 3, 4, 5, 6, 7, 8]
235
238
  try:
236
239
  resource_types = [int(x.strip()) for x in resource_choice.split(",")]
237
- if any(i < 1 or i > 7 for i in resource_types):
238
- Console.write_stdout("Error: Invalid selection. Numbers must be between 1 and 7.")
240
+ if any(i < 1 or i > 8 for i in resource_types):
241
+ Console.write_stdout("Error: Invalid selection. Numbers must be between 1 and 8.")
239
242
  continue
240
243
  return resource_types
241
244
  except ValueError:
@@ -368,6 +371,29 @@ def fetch_and_select_files(
368
371
  return None
369
372
 
370
373
 
374
+ def fetch_and_select_secrets(from_api_key: str, from_instance: str) -> Optional[str]:
375
+ """
376
+ Fetch and prompt user to select secrets for migration.
377
+
378
+ :param from_api_key: Source API key
379
+ :param from_instance: Source instance URL
380
+ :return: Comma-separated secret IDs, 'all', or None
381
+ """
382
+ try:
383
+ secret_client = SecretClient(api_key=from_api_key, base_url=from_instance)
384
+ secrets_data = secret_client.list_secrets(count=1000)
385
+ secrets_list = secrets_data.get("secrets", []) if isinstance(secrets_data, dict) else []
386
+
387
+ if secrets_list:
388
+ secrets_objects = [type('obj', (object,), {'id': s.get('id'), 'name': s.get('name')})()
389
+ for s in secrets_list if s.get('id')]
390
+ selection = prompt_resource_selection("secrets", secrets_objects, id_field="id", name_field="name")
391
+ return selection
392
+ except Exception as e:
393
+ Console.write_stdout(f"Warning: Could not retrieve secrets: {e}")
394
+ return None
395
+
396
+
371
397
  def handle_usage_limits_keys(
372
398
  same_instance: bool,
373
399
  from_organization_api_key: Optional[str],
@@ -554,6 +580,11 @@ def clone_project_interactively() -> None:
554
580
  )
555
581
  selected_resources["usage_limits"] = True
556
582
 
583
+ if 8 in resource_types_to_migrate:
584
+ selection = fetch_and_select_secrets(from_api_key, from_instance)
585
+ if selection:
586
+ selected_resources["secrets"] = selection
587
+
557
588
  confirmed, stop_on_error = show_summary_and_confirm(from_instance, from_project_id, to_instance, to_project_name, to_project_id, selected_resources)
558
589
  if not confirmed:
559
590
  Console.write_stdout("Migration cancelled.")
@@ -571,7 +602,7 @@ def clone_project(option_list: list) -> None:
571
602
  Clone a project with selected components from source to destination instance.
572
603
 
573
604
  Supports migration of agents, tools, agentic processes, tasks, usage limits,
574
- RAG assistants, and files between GenExus AI instances.
605
+ RAG assistants, files, and secrets between GEAI instances.
575
606
 
576
607
  :param option_list: List of (option_flag, option_value) tuples from CLI parsing
577
608
  """
@@ -606,6 +637,7 @@ def clone_project(option_list: list) -> None:
606
637
  migrate_usage_limits = False
607
638
  migrate_rag_assistants = False
608
639
  migrate_files = False
640
+ migrate_secrets = False
609
641
 
610
642
  agent_ids = None
611
643
  tool_ids = None
@@ -613,6 +645,7 @@ def clone_project(option_list: list) -> None:
613
645
  task_ids = None
614
646
  assistant_names = None
615
647
  file_ids = None
648
+ secret_ids = None
616
649
 
617
650
  stop_on_error = True
618
651
 
@@ -663,6 +696,9 @@ def clone_project(option_list: list) -> None:
663
696
  elif option_flag.name == "files":
664
697
  migrate_files = True
665
698
  file_ids = option_arg if option_arg else "all"
699
+ elif option_flag.name == "secrets":
700
+ migrate_secrets = True
701
+ secret_ids = option_arg if option_arg else "all"
666
702
  elif option_flag.name == "stop_on_error":
667
703
  from pygeai.cli.commands.common import get_boolean_value
668
704
  stop_on_error = get_boolean_value(option_arg)
@@ -778,12 +814,14 @@ def clone_project(option_list: list) -> None:
778
814
  migrate_usage_limits = True if from_organization_id and to_organization_id else False
779
815
  migrate_rag_assistants = True
780
816
  migrate_files = True if from_organization_id and to_organization_id else False
817
+ migrate_secrets = True
781
818
  agent_ids = "all"
782
819
  tool_ids = "all"
783
820
  process_ids = "all"
784
821
  task_ids = "all"
785
822
  assistant_names = "all"
786
823
  file_ids = "all"
824
+ secret_ids = "all"
787
825
 
788
826
  strategies = []
789
827
 
@@ -954,6 +992,31 @@ def clone_project(option_list: list) -> None:
954
992
  to_instance=to_instance
955
993
  ))
956
994
 
995
+ if migrate_secrets:
996
+ secret_client = SecretClient(api_key=from_api_key, base_url=from_instance)
997
+ if secret_ids == "all":
998
+ secrets_data = secret_client.list_secrets(count=1000)
999
+ secrets_list = secrets_data.get("secrets", []) if isinstance(secrets_data, dict) else []
1000
+ discovered_secrets = [s.get('id') for s in secrets_list if s.get('id')]
1001
+ Console.write_stdout(f"Discovered {len(discovered_secrets)} secrets")
1002
+ for secret_id in discovered_secrets:
1003
+ strategies.append(SecretMigrationStrategy(
1004
+ from_api_key=from_api_key,
1005
+ from_instance=from_instance,
1006
+ secret_id=secret_id,
1007
+ to_api_key=to_api_key,
1008
+ to_instance=to_instance
1009
+ ))
1010
+ elif secret_ids:
1011
+ for secret_id in secret_ids.split(','):
1012
+ strategies.append(SecretMigrationStrategy(
1013
+ from_api_key=from_api_key,
1014
+ from_instance=from_instance,
1015
+ secret_id=secret_id.strip(),
1016
+ to_api_key=to_api_key,
1017
+ to_instance=to_instance
1018
+ ))
1019
+
957
1020
  if not strategies:
958
1021
  Console.write_stdout("No migration strategies configured. Use flags like --agents, --tools, --all, etc.")
959
1022
  return
@@ -1098,6 +1161,12 @@ clone_project_options = [
1098
1161
  "File IDs to migrate: comma-separated IDs or 'all' (requires org/project IDs)",
1099
1162
  True
1100
1163
  ),
1164
+ Option(
1165
+ "secrets",
1166
+ ["--secrets"],
1167
+ "Secret IDs to migrate: comma-separated IDs or 'all'",
1168
+ True
1169
+ ),
1101
1170
  Option(
1102
1171
  "stop_on_error",
1103
1172
  ["--stop-on-error", "--soe"],
@@ -484,6 +484,235 @@ get_organization_members_options = [
484
484
  ),
485
485
  ]
486
486
 
487
+
488
+ def add_project_member(option_list: list):
489
+ project_id = None
490
+ user_email = None
491
+ roles = []
492
+ batch_file = None
493
+
494
+ for option_flag, option_arg in option_list:
495
+ if option_flag.name == "project_id":
496
+ project_id = option_arg
497
+ if option_flag.name == "user_email":
498
+ user_email = option_arg
499
+ if option_flag.name == "roles":
500
+ roles = [r.strip() for r in option_arg.split(",")]
501
+ if option_flag.name == "batch_file":
502
+ batch_file = option_arg
503
+
504
+ client = OrganizationClient()
505
+
506
+ if batch_file:
507
+ add_project_member_in_batch(client, batch_file)
508
+ else:
509
+ if not (project_id and user_email and roles):
510
+ raise MissingRequirementException("Cannot add project member without project-id, user email, and roles")
511
+ result = client.add_project_member(project_id, user_email, roles)
512
+ Console.write_stdout(f"User invitation sent: \n{result}")
513
+
514
+
515
+ def add_project_member_in_batch(client: OrganizationClient, batch_file: str):
516
+ import csv
517
+ import os
518
+
519
+ if not os.path.exists(batch_file):
520
+ raise MissingRequirementException(f"Batch file not found: {batch_file}")
521
+
522
+ successful = 0
523
+ failed = 0
524
+ errors = []
525
+
526
+ try:
527
+ with open(batch_file, 'r') as f:
528
+ csv_reader = csv.reader(f)
529
+ for line_num, row in enumerate(csv_reader, start=1):
530
+ if len(row) < 3:
531
+ error_msg = f"Line {line_num}: Invalid format - expected at least 3 columns (project_id, email, role1, ...)"
532
+ errors.append(error_msg)
533
+ failed += 1
534
+ continue
535
+
536
+ project_id = row[0].strip()
537
+ email = row[1].strip()
538
+ roles = [r.strip() for r in row[2:] if r.strip()]
539
+
540
+ if not (project_id and email and roles):
541
+ error_msg = f"Line {line_num}: Missing required fields (project_id={project_id}, email={email}, roles={roles})"
542
+ errors.append(error_msg)
543
+ failed += 1
544
+ continue
545
+
546
+ try:
547
+ client.add_project_member(project_id, email, roles)
548
+ successful += 1
549
+ except Exception as e:
550
+ error_msg = f"Line {line_num}: Failed to add {email} to project {project_id}: {str(e)}"
551
+ errors.append(error_msg)
552
+ failed += 1
553
+
554
+ Console.write_stdout(f"Batch processing complete: {successful} successful, {failed} failed")
555
+ if errors:
556
+ Console.write_stdout("\nErrors:")
557
+ for error in errors:
558
+ Console.write_stdout(f" - {error}")
559
+ except Exception as e:
560
+ raise MissingRequirementException(f"Failed to read batch file: {str(e)}")
561
+
562
+
563
+ add_project_member_options = [
564
+ Option(
565
+ "project_id",
566
+ ["--project-id", "--pid"],
567
+ "GUID of the project (required unless --batch is used)",
568
+ True
569
+ ),
570
+ Option(
571
+ "user_email",
572
+ ["--email", "-e"],
573
+ "Email address of the user to invite (required unless --batch is used)",
574
+ True
575
+ ),
576
+ Option(
577
+ "roles",
578
+ ["--roles", "-r"],
579
+ "Comma-separated list of role names or GUIDs (e.g., 'Project member,Project administrator') (required unless --batch is used)",
580
+ True
581
+ ),
582
+ Option(
583
+ "batch_file",
584
+ ["--batch", "-b"],
585
+ "Path to CSV file with format: project_id,email,role1,role2,... (one invitation per line)",
586
+ True
587
+ ),
588
+ ]
589
+
590
+
591
+ def create_organization(option_list: list):
592
+ name = None
593
+ email = None
594
+
595
+ for option_flag, option_arg in option_list:
596
+ if option_flag.name == "name":
597
+ name = option_arg
598
+
599
+ if option_flag.name == "admin_email":
600
+ email = option_arg
601
+
602
+ if not (name and email):
603
+ raise MissingRequirementException("Cannot create organization without name and administrator's email")
604
+
605
+ client = OrganizationClient()
606
+ result = client.create_organization(name, email)
607
+ Console.write_stdout(f"New organization: \\n{result}")
608
+
609
+
610
+ create_organization_options = [
611
+ Option(
612
+ "name",
613
+ ["--name", "-n"],
614
+ "Name of the new organization",
615
+ True
616
+ ),
617
+ Option(
618
+ "admin_email",
619
+ ["--email", "-e"],
620
+ "Organization administrator's email",
621
+ True
622
+ ),
623
+ ]
624
+
625
+
626
+ def get_organization_list(option_list: list):
627
+ start_page = None
628
+ page_size = None
629
+ order_key = None
630
+ order_direction = "desc"
631
+ filter_key = None
632
+ filter_value = None
633
+
634
+ for option_flag, option_arg in option_list:
635
+ if option_flag.name == "start_page":
636
+ start_page = int(option_arg)
637
+ if option_flag.name == "page_size":
638
+ page_size = int(option_arg)
639
+ if option_flag.name == "order_key":
640
+ order_key = option_arg
641
+ if option_flag.name == "order_direction":
642
+ order_direction = option_arg
643
+ if option_flag.name == "filter_key":
644
+ filter_key = option_arg
645
+ if option_flag.name == "filter_value":
646
+ filter_value = option_arg
647
+
648
+ client = OrganizationClient()
649
+ result = client.get_organization_list(start_page, page_size, order_key, order_direction, filter_key, filter_value)
650
+ Console.write_stdout(f"Organization list: \\n{result}")
651
+
652
+
653
+ get_organization_list_options = [
654
+ Option(
655
+ "start_page",
656
+ ["--start-page"],
657
+ "Page number for pagination",
658
+ True
659
+ ),
660
+ Option(
661
+ "page_size",
662
+ ["--page-size"],
663
+ "Number of items per page",
664
+ True
665
+ ),
666
+ Option(
667
+ "order_key",
668
+ ["--order-key"],
669
+ "Field for sorting (only 'name' supported)",
670
+ True
671
+ ),
672
+ Option(
673
+ "order_direction",
674
+ ["--order-direction"],
675
+ "Sort direction: asc or desc (default: desc)",
676
+ True
677
+ ),
678
+ Option(
679
+ "filter_key",
680
+ ["--filter-key"],
681
+ "Field for filtering (only 'name' supported)",
682
+ True
683
+ ),
684
+ Option(
685
+ "filter_value",
686
+ ["--filter-value"],
687
+ "Value to filter by",
688
+ True
689
+ ),
690
+ ]
691
+
692
+
693
+ def delete_organization(option_list: list):
694
+ organization_id = None
695
+ for option_flag, option_arg in option_list:
696
+ if option_flag.name == "organization_id":
697
+ organization_id = option_arg
698
+
699
+ if not organization_id:
700
+ raise MissingRequirementException("Cannot delete organization without organization-id")
701
+
702
+ client = OrganizationClient()
703
+ result = client.delete_organization(organization_id)
704
+ Console.write_stdout(f"Deleted organization: \\n{result}")
705
+
706
+
707
+ delete_organization_options = [
708
+ Option(
709
+ "organization_id",
710
+ ["--organization-id", "--oid"],
711
+ "GUID of the organization (required)",
712
+ True
713
+ ),
714
+ ]
715
+
487
716
  organization_commands = [
488
717
  Command(
489
718
  "help",
@@ -611,4 +840,40 @@ organization_commands = [
611
840
  [],
612
841
  get_organization_members_options
613
842
  ),
843
+ Command(
844
+ "add_project_member",
845
+ ["add-project-member", "apm"],
846
+ "Add a user to a project by sending an invitation email",
847
+ add_project_member,
848
+ ArgumentsEnum.REQUIRED,
849
+ [],
850
+ add_project_member_options
851
+ ),
852
+ Command(
853
+ "create_organization",
854
+ ["create-organization"],
855
+ "Create new organization",
856
+ create_organization,
857
+ ArgumentsEnum.REQUIRED,
858
+ [],
859
+ create_organization_options
860
+ ),
861
+ Command(
862
+ "organization_list",
863
+ ["list-organizations"],
864
+ "List organization information",
865
+ get_organization_list,
866
+ ArgumentsEnum.OPTIONAL,
867
+ [],
868
+ get_organization_list_options
869
+ ),
870
+ Command(
871
+ "delete_organization",
872
+ ["delete-organization"],
873
+ "Delete existing organization",
874
+ delete_organization,
875
+ ArgumentsEnum.REQUIRED,
876
+ [],
877
+ delete_organization_options
878
+ ),
614
879
  ]
@@ -1,6 +1,8 @@
1
1
  from pathlib import Path
2
+ from typing import Any, Dict, List, Optional, Union
3
+ import json
2
4
 
3
- from pygeai.core.common.exceptions import WrongArgumentError
5
+ from pygeai.core.common.exceptions import WrongArgumentError, ValidationError
4
6
 
5
7
 
6
8
  def validate_dataset_file(dataset_file: str):
@@ -64,3 +66,144 @@ def validate_system_metric(metric: dict):
64
66
 
65
67
  if not isinstance(metric["systemMetricWeight"], (int, float)) or not (0 <= metric["systemMetricWeight"] <= 1):
66
68
  raise WrongArgumentError('"systemMetricWeight" must be a number between 0 and 1 (inclusive).')
69
+
70
+
71
+ def validate_json_input(
72
+ value: str,
73
+ expected_type: type,
74
+ field_name: str,
75
+ example: Optional[str] = None
76
+ ) -> Union[Dict, List]:
77
+ """
78
+ Validates and parses JSON input with detailed error messages.
79
+
80
+ :param value: str - The JSON string to parse.
81
+ :param expected_type: type - Expected type (dict or list).
82
+ :param field_name: str - Name of the field being validated.
83
+ :param example: Optional[str] - Example of valid input.
84
+ :return: Union[Dict, List] - Parsed JSON object.
85
+ :raises ValidationError: If JSON is invalid or doesn't match expected type.
86
+ """
87
+ try:
88
+ parsed = json.loads(value)
89
+ except json.JSONDecodeError as e:
90
+ raise ValidationError(
91
+ f"Invalid JSON for '{field_name}'",
92
+ field=field_name,
93
+ expected="Valid JSON string",
94
+ received=value[:100] + "..." if len(value) > 100 else value,
95
+ example=example or '{"key": "value"}'
96
+ ) from e
97
+
98
+ if not isinstance(parsed, expected_type):
99
+ raise ValidationError(
100
+ f"Wrong type for '{field_name}'",
101
+ field=field_name,
102
+ expected=expected_type.__name__,
103
+ received=type(parsed).__name__,
104
+ example=example
105
+ )
106
+
107
+ return parsed
108
+
109
+
110
+ def validate_numeric_range(
111
+ value: str,
112
+ field_name: str,
113
+ min_value: Optional[float] = None,
114
+ max_value: Optional[float] = None,
115
+ value_type: type = float
116
+ ) -> Union[int, float]:
117
+ """
118
+ Validates numeric input within a specified range.
119
+
120
+ :param value: str - The numeric string to validate.
121
+ :param field_name: str - Name of the field being validated.
122
+ :param min_value: Optional[float] - Minimum allowed value.
123
+ :param max_value: Optional[float] - Maximum allowed value.
124
+ :param value_type: type - Expected numeric type (int or float).
125
+ :return: Union[int, float] - Validated numeric value.
126
+ :raises ValidationError: If value is not numeric or out of range.
127
+ """
128
+ try:
129
+ numeric_value = value_type(value)
130
+ except ValueError as e:
131
+ raise ValidationError(
132
+ f"Invalid {value_type.__name__} for '{field_name}'",
133
+ field=field_name,
134
+ expected=f"{value_type.__name__}",
135
+ received=value
136
+ ) from e
137
+
138
+ if min_value is not None and numeric_value < min_value:
139
+ raise ValidationError(
140
+ f"Value too low for '{field_name}'",
141
+ field=field_name,
142
+ expected=f">= {min_value}",
143
+ received=str(numeric_value)
144
+ )
145
+
146
+ if max_value is not None and numeric_value > max_value:
147
+ raise ValidationError(
148
+ f"Value too high for '{field_name}'",
149
+ field=field_name,
150
+ expected=f"<= {max_value}",
151
+ received=str(numeric_value)
152
+ )
153
+
154
+ return numeric_value
155
+
156
+
157
+ def validate_choice(
158
+ value: str,
159
+ field_name: str,
160
+ choices: List[str],
161
+ case_sensitive: bool = True
162
+ ) -> str:
163
+ """
164
+ Validates that input is one of the allowed choices.
165
+
166
+ :param value: str - The value to validate.
167
+ :param field_name: str - Name of the field being validated.
168
+ :param choices: List[str] - List of allowed values.
169
+ :param case_sensitive: bool - Whether comparison is case-sensitive.
170
+ :return: str - Validated value.
171
+ :raises ValidationError: If value is not in choices.
172
+ """
173
+ compare_value = value if case_sensitive else value.lower()
174
+ compare_choices = choices if case_sensitive else [c.lower() for c in choices]
175
+
176
+ if compare_value not in compare_choices:
177
+ raise ValidationError(
178
+ f"Invalid value for '{field_name}'",
179
+ field=field_name,
180
+ expected=f"One of: {', '.join(choices)}",
181
+ received=value
182
+ )
183
+
184
+ return value
185
+
186
+
187
+ def validate_boolean_value(value: str, field_name: str = "boolean option") -> bool:
188
+ """
189
+ Converts a string argument into a boolean value with flexible input formats.
190
+
191
+ :param value: str - A string representation of a boolean.
192
+ :param field_name: str - Name of the field being validated.
193
+ :return: bool - The boolean value corresponding to the input.
194
+ :raises ValidationError: If the input is not a valid boolean representation.
195
+ """
196
+ normalized = value.lower().strip()
197
+
198
+ if normalized in ("0", "false", "no", "off"):
199
+ return False
200
+ elif normalized in ("1", "true", "yes", "on"):
201
+ return True
202
+ else:
203
+ raise ValidationError(
204
+ f"Invalid boolean value for '{field_name}'",
205
+ field=field_name,
206
+ expected="0/1, true/false, yes/no, on/off",
207
+ received=value,
208
+ example="1 or true"
209
+ )
@@ -1,12 +1,16 @@
1
1
  import traceback
2
2
  from difflib import SequenceMatcher
3
- from typing import List, Optional
3
+ from typing import List, Optional, Tuple
4
4
 
5
5
  from pygeai import logger
6
6
  from pygeai.cli.commands import Command, Option
7
7
  from pygeai.core.utils.console import Console
8
8
 
9
9
 
10
+ FUZZY_MATCH_THRESHOLD = 0.6
11
+ MAX_FUZZY_SUGGESTIONS = 3
12
+
13
+
10
14
  class ExitCode:
11
15
  SUCCESS = 0
12
16
  USER_INPUT_ERROR = 1
@@ -19,27 +23,58 @@ class ExitCode:
19
23
  class ErrorHandler:
20
24
 
21
25
  @staticmethod
22
- def format_error(error_type: str, message: str, suggestion: Optional[str] = None, show_help: bool = True) -> str:
23
- output = f"ERROR: {message}"
26
+ def format_error(
27
+ error_type: str,
28
+ message: str,
29
+ suggestion: Optional[str] = None,
30
+ show_help: bool = True,
31
+ example: Optional[str] = None
32
+ ) -> str:
33
+ """
34
+ Formats an error message with optional suggestion and example.
35
+
36
+ :param error_type: str - Type of error (e.g., "Unknown Command").
37
+ :param message: str - The error message.
38
+ :param suggestion: Optional[str] - Suggested fix or next steps.
39
+ :param show_help: bool - Whether to show help command hint.
40
+ :param example: Optional[str] - Example of correct usage.
41
+ :return: str - Formatted error message.
42
+ """
43
+ output = f"ERROR [{error_type}]: {message}"
24
44
 
25
45
  if suggestion:
26
46
  output += f"\n → {suggestion}"
27
47
 
48
+ if example:
49
+ output += f"\n\n Example:\n {example}"
50
+
28
51
  if show_help:
29
52
  output += "\n\nRun 'geai help' for usage information."
30
53
 
31
54
  return output
32
55
 
33
56
  @staticmethod
34
- def find_similar_items(item: str, available_items: List[str], threshold: float = 0.6) -> List[str]:
35
- similarities = []
57
+ def find_similar_items(
58
+ item: str,
59
+ available_items: List[str],
60
+ threshold: float = FUZZY_MATCH_THRESHOLD
61
+ ) -> List[str]:
62
+ """
63
+ Finds similar items using fuzzy string matching.
64
+
65
+ :param item: str - The item to match against.
66
+ :param available_items: List[str] - List of available items.
67
+ :param threshold: float - Minimum similarity ratio (0.0 to 1.0).
68
+ :return: List[str] - List of similar items, up to MAX_FUZZY_SUGGESTIONS.
69
+ """
70
+ similarities: List[Tuple[str, float]] = []
36
71
  for available in available_items:
37
72
  ratio = SequenceMatcher(None, item.lower(), available.lower()).ratio()
38
73
  if ratio >= threshold:
39
74
  similarities.append((available, ratio))
40
75
 
41
76
  similarities.sort(key=lambda x: x[1], reverse=True)
42
- return [item[0] for item in similarities[:3]]
77
+ return [item[0] for item in similarities[:MAX_FUZZY_SUGGESTIONS]]
43
78
 
44
79
  @staticmethod
45
80
  def get_available_commands(commands: List[Command]) -> List[str]: