pygeai 0.6.0b10__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 (135) 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/chat/clients.py +46 -1
  30. pygeai/chat/endpoints.py +1 -0
  31. pygeai/cli/commands/analytics.py +525 -0
  32. pygeai/cli/commands/base.py +16 -0
  33. pygeai/cli/commands/chat.py +95 -0
  34. pygeai/cli/commands/common.py +28 -24
  35. pygeai/cli/commands/migrate.py +75 -6
  36. pygeai/cli/commands/organization.py +265 -0
  37. pygeai/cli/commands/validators.py +144 -1
  38. pygeai/cli/error_handler.py +41 -6
  39. pygeai/cli/geai.py +99 -16
  40. pygeai/cli/parsers.py +75 -31
  41. pygeai/cli/texts/help.py +75 -6
  42. pygeai/core/base/clients.py +18 -4
  43. pygeai/core/base/session.py +46 -7
  44. pygeai/core/common/config.py +25 -2
  45. pygeai/core/common/exceptions.py +64 -1
  46. pygeai/core/services/rest.py +20 -2
  47. pygeai/evaluation/clients.py +5 -3
  48. pygeai/lab/agents/clients.py +3 -3
  49. pygeai/lab/agents/endpoints.py +2 -2
  50. pygeai/lab/agents/mappers.py +50 -2
  51. pygeai/lab/clients.py +5 -2
  52. pygeai/lab/managers.py +7 -9
  53. pygeai/lab/models.py +70 -2
  54. pygeai/lab/tools/clients.py +1 -59
  55. pygeai/migration/__init__.py +3 -1
  56. pygeai/migration/strategies.py +72 -3
  57. pygeai/organization/clients.py +110 -1
  58. pygeai/organization/endpoints.py +11 -7
  59. pygeai/organization/managers.py +134 -2
  60. pygeai/organization/mappers.py +28 -2
  61. pygeai/organization/responses.py +11 -1
  62. pygeai/tests/analytics/__init__.py +0 -0
  63. pygeai/tests/analytics/test_clients.py +86 -0
  64. pygeai/tests/analytics/test_managers.py +94 -0
  65. pygeai/tests/analytics/test_mappers.py +84 -0
  66. pygeai/tests/analytics/test_responses.py +73 -0
  67. pygeai/tests/auth/test_oauth.py +172 -0
  68. pygeai/tests/cli/commands/test_migrate.py +14 -1
  69. pygeai/tests/cli/commands/test_organization.py +69 -1
  70. pygeai/tests/cli/test_error_handler.py +4 -4
  71. pygeai/tests/cli/test_geai_driver.py +1 -1
  72. pygeai/tests/lab/agents/test_mappers.py +128 -1
  73. pygeai/tests/lab/test_models.py +2 -0
  74. pygeai/tests/lab/tools/test_clients.py +2 -31
  75. pygeai/tests/organization/test_clients.py +180 -1
  76. pygeai/tests/organization/test_managers.py +40 -0
  77. pygeai/tests/snippets/analytics/__init__.py +0 -0
  78. pygeai/tests/snippets/analytics/get_agent_usage_per_user.py +16 -0
  79. pygeai/tests/snippets/analytics/get_agents_created_and_modified.py +11 -0
  80. pygeai/tests/snippets/analytics/get_average_cost_per_request.py +10 -0
  81. pygeai/tests/snippets/analytics/get_overall_error_rate.py +10 -0
  82. pygeai/tests/snippets/analytics/get_top_10_agents_by_requests.py +12 -0
  83. pygeai/tests/snippets/analytics/get_total_active_users.py +10 -0
  84. pygeai/tests/snippets/analytics/get_total_cost.py +10 -0
  85. pygeai/tests/snippets/analytics/get_total_requests_per_day.py +12 -0
  86. pygeai/tests/snippets/analytics/get_total_tokens.py +12 -0
  87. pygeai/tests/snippets/chat/get_response_complete_example.py +67 -0
  88. pygeai/tests/snippets/chat/get_response_with_instructions.py +19 -0
  89. pygeai/tests/snippets/chat/get_response_with_metadata.py +24 -0
  90. pygeai/tests/snippets/chat/get_response_with_parallel_tools.py +58 -0
  91. pygeai/tests/snippets/chat/get_response_with_reasoning.py +21 -0
  92. pygeai/tests/snippets/chat/get_response_with_store.py +38 -0
  93. pygeai/tests/snippets/chat/get_response_with_truncation.py +24 -0
  94. pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +39 -0
  95. pygeai/tests/snippets/lab/agents/create_agent_with_properties.py +46 -0
  96. pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +62 -0
  97. pygeai/tests/snippets/lab/agents/update_agent_properties.py +50 -0
  98. pygeai/tests/snippets/organization/add_project_member.py +10 -0
  99. pygeai/tests/snippets/organization/add_project_member_batch.py +44 -0
  100. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/METADATA +1 -1
  101. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/RECORD +105 -95
  102. pygeai/_docs/source/pygeai.tests.snippets.assistants.data_analyst.rst +0 -37
  103. pygeai/_docs/source/pygeai.tests.snippets.assistants.rag.rst +0 -85
  104. pygeai/_docs/source/pygeai.tests.snippets.assistants.rst +0 -78
  105. pygeai/_docs/source/pygeai.tests.snippets.auth.rst +0 -10
  106. pygeai/_docs/source/pygeai.tests.snippets.chat.rst +0 -125
  107. pygeai/_docs/source/pygeai.tests.snippets.dbg.rst +0 -45
  108. pygeai/_docs/source/pygeai.tests.snippets.embeddings.rst +0 -61
  109. pygeai/_docs/source/pygeai.tests.snippets.evaluation.dataset.rst +0 -197
  110. pygeai/_docs/source/pygeai.tests.snippets.evaluation.plan.rst +0 -133
  111. pygeai/_docs/source/pygeai.tests.snippets.evaluation.result.rst +0 -37
  112. pygeai/_docs/source/pygeai.tests.snippets.evaluation.rst +0 -20
  113. pygeai/_docs/source/pygeai.tests.snippets.extras.rst +0 -37
  114. pygeai/_docs/source/pygeai.tests.snippets.files.rst +0 -53
  115. pygeai/_docs/source/pygeai.tests.snippets.gam.rst +0 -21
  116. pygeai/_docs/source/pygeai.tests.snippets.lab.agents.rst +0 -93
  117. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.jobs.rst +0 -21
  118. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.kbs.rst +0 -45
  119. pygeai/_docs/source/pygeai.tests.snippets.lab.processes.rst +0 -46
  120. pygeai/_docs/source/pygeai.tests.snippets.lab.rst +0 -82
  121. pygeai/_docs/source/pygeai.tests.snippets.lab.samples.rst +0 -21
  122. pygeai/_docs/source/pygeai.tests.snippets.lab.strategies.rst +0 -45
  123. pygeai/_docs/source/pygeai.tests.snippets.lab.tools.rst +0 -85
  124. pygeai/_docs/source/pygeai.tests.snippets.lab.use_cases.rst +0 -117
  125. pygeai/_docs/source/pygeai.tests.snippets.migrate.rst +0 -10
  126. pygeai/_docs/source/pygeai.tests.snippets.organization.rst +0 -109
  127. pygeai/_docs/source/pygeai.tests.snippets.rag.rst +0 -85
  128. pygeai/_docs/source/pygeai.tests.snippets.rerank.rst +0 -21
  129. pygeai/_docs/source/pygeai.tests.snippets.rst +0 -32
  130. pygeai/_docs/source/pygeai.tests.snippets.secrets.rst +0 -10
  131. pygeai/_docs/source/pygeai.tests.snippets.usage_limit.rst +0 -77
  132. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/WHEEL +0 -0
  133. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/entry_points.txt +0 -0
  134. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/licenses/LICENSE +0 -0
  135. {pygeai-0.6.0b10.dist-info → pygeai-0.6.0b12.dist-info}/top_level.txt +0 -0
@@ -109,39 +109,43 @@ def get_messages(message_list: list):
109
109
  return messages
110
110
 
111
111
 
112
- def get_boolean_value(option_arg: str):
112
+ def get_boolean_value(option_arg: str) -> bool:
113
113
  """
114
- Converts a string argument into a boolean value.
115
-
116
- :param option_arg: str - A string representation of an integer (e.g., "0" or "1") to be converted to a boolean.
117
- "0" is interpreted as `False`, and "1" is interpreted as `True`.
118
- :return: bool - The boolean value corresponding to the input.
119
- :raises WrongArgumentError: If the input is not a valid integer or is not "0" or "1".
120
- Possible values for `option_arg` are "0" (off) or "1" (on).
121
- """
122
- try:
123
- return bool(int(option_arg))
124
- except ValueError as e:
114
+ Converts a string argument into a boolean value with flexible input formats.
115
+
116
+ :param option_arg: str - A string representation of a boolean.
117
+ Accepts: "0"/"1", "true"/"false", "yes"/"no", "on"/"off".
118
+ :return: bool - The boolean value corresponding to the input.
119
+ :raises WrongArgumentError: If the input is not a valid boolean representation.
120
+ """
121
+ normalized = option_arg.lower().strip()
122
+
123
+ if normalized in ("0", "false", "no", "off"):
124
+ return False
125
+ elif normalized in ("1", "true", "yes", "on"):
126
+ return True
127
+ else:
125
128
  raise WrongArgumentError("Possible values are 0 or 1, for off and on, respectively.")
126
129
 
127
130
 
128
- def get_penalty_float_value(option_arg: str):
131
+ def get_penalty_float_value(option_arg: str) -> float:
129
132
  """
130
- Converts a string argument into a float value representing a penalty and validates its range.
133
+ Converts a string argument into a float value representing a penalty and validates its range.
131
134
 
132
- :param option_arg: str - A string representation of a float to be converted to a penalty value.
135
+ :param option_arg: str - A string representation of a float to be converted to a penalty value.
133
136
  The value must be between -2.0 and 2.0 (inclusive).
134
- :return: float - The float value corresponding to the input, if valid.
135
- :raises WrongArgumentError: If the input is not a valid float or if the value is outside the range [-2.0, 2.0].
136
- The penalty must be a number between -2.0 and 2.0.
137
- """
137
+ :return: float - The float value corresponding to the input, if valid.
138
+ :raises WrongArgumentError: If the input is not a valid float or if the value is outside the range [-2.0, 2.0].
139
+ """
138
140
  try:
139
- penalty = float(option_arg)
140
- if penalty < -2.0 or 2.0 < penalty:
141
- raise ValueError("Penalty out of range")
142
- return penalty
143
- except Exception as e:
141
+ penalty_value = float(option_arg)
142
+ except ValueError:
143
+ raise WrongArgumentError("If defined, penalty must be a number between -2.0 and 2.0")
144
+
145
+ if not (-2.0 <= penalty_value <= 2.0):
144
146
  raise WrongArgumentError("If defined, penalty must be a number between -2.0 and 2.0")
147
+
148
+ return penalty_value
145
149
 
146
150
 
147
151
  def _build_llm_options(
@@ -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
+ )