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.
- pygeai/_docs/source/content/ai_lab/cli.rst +4 -4
- pygeai/_docs/source/content/ai_lab/models.rst +169 -35
- pygeai/_docs/source/content/ai_lab/runner.rst +2 -2
- pygeai/_docs/source/content/ai_lab/spec.rst +9 -9
- pygeai/_docs/source/content/ai_lab/usage.rst +34 -34
- pygeai/_docs/source/content/ai_lab.rst +1 -1
- pygeai/_docs/source/content/analytics.rst +598 -0
- pygeai/_docs/source/content/api_reference/chat.rst +428 -2
- pygeai/_docs/source/content/api_reference/embeddings.rst +1 -1
- pygeai/_docs/source/content/api_reference/project.rst +184 -0
- pygeai/_docs/source/content/api_reference/rag.rst +2 -2
- pygeai/_docs/source/content/authentication.rst +295 -0
- pygeai/_docs/source/content/cli.rst +79 -2
- pygeai/_docs/source/content/debugger.rst +1 -1
- pygeai/_docs/source/content/migration.rst +19 -2
- pygeai/_docs/source/index.rst +2 -0
- pygeai/_docs/source/pygeai.analytics.rst +53 -0
- pygeai/_docs/source/pygeai.cli.commands.rst +8 -0
- pygeai/_docs/source/pygeai.rst +1 -0
- pygeai/_docs/source/pygeai.tests.analytics.rst +45 -0
- pygeai/_docs/source/pygeai.tests.auth.rst +8 -0
- pygeai/_docs/source/pygeai.tests.rst +1 -1
- pygeai/analytics/__init__.py +0 -0
- pygeai/analytics/clients.py +505 -0
- pygeai/analytics/endpoints.py +35 -0
- pygeai/analytics/managers.py +606 -0
- pygeai/analytics/mappers.py +207 -0
- pygeai/analytics/responses.py +240 -0
- pygeai/cli/commands/analytics.py +525 -0
- pygeai/cli/commands/base.py +16 -0
- pygeai/cli/commands/common.py +28 -24
- pygeai/cli/commands/migrate.py +75 -6
- pygeai/cli/commands/organization.py +265 -0
- pygeai/cli/commands/validators.py +144 -1
- pygeai/cli/error_handler.py +41 -6
- pygeai/cli/geai.py +99 -16
- pygeai/cli/parsers.py +75 -31
- pygeai/cli/texts/help.py +75 -6
- pygeai/core/base/clients.py +18 -4
- pygeai/core/base/session.py +46 -7
- pygeai/core/common/config.py +25 -2
- pygeai/core/common/exceptions.py +64 -1
- pygeai/core/services/rest.py +20 -2
- pygeai/evaluation/clients.py +5 -3
- pygeai/lab/agents/clients.py +3 -3
- pygeai/lab/agents/endpoints.py +2 -2
- pygeai/lab/agents/mappers.py +50 -2
- pygeai/lab/clients.py +5 -2
- pygeai/lab/managers.py +7 -9
- pygeai/lab/models.py +70 -2
- pygeai/lab/tools/clients.py +1 -59
- pygeai/migration/__init__.py +3 -1
- pygeai/migration/strategies.py +72 -3
- pygeai/organization/clients.py +110 -1
- pygeai/organization/endpoints.py +11 -7
- pygeai/organization/managers.py +134 -2
- pygeai/organization/mappers.py +28 -2
- pygeai/organization/responses.py +11 -1
- pygeai/tests/analytics/__init__.py +0 -0
- pygeai/tests/analytics/test_clients.py +86 -0
- pygeai/tests/analytics/test_managers.py +94 -0
- pygeai/tests/analytics/test_mappers.py +84 -0
- pygeai/tests/analytics/test_responses.py +73 -0
- pygeai/tests/auth/test_oauth.py +172 -0
- pygeai/tests/cli/commands/test_migrate.py +14 -1
- pygeai/tests/cli/commands/test_organization.py +69 -1
- pygeai/tests/cli/test_error_handler.py +4 -4
- pygeai/tests/cli/test_geai_driver.py +1 -1
- pygeai/tests/lab/agents/test_mappers.py +128 -1
- pygeai/tests/lab/test_models.py +2 -0
- pygeai/tests/lab/tools/test_clients.py +2 -31
- pygeai/tests/organization/test_clients.py +180 -1
- pygeai/tests/organization/test_managers.py +40 -0
- pygeai/tests/snippets/analytics/__init__.py +0 -0
- pygeai/tests/snippets/analytics/get_agent_usage_per_user.py +16 -0
- pygeai/tests/snippets/analytics/get_agents_created_and_modified.py +11 -0
- pygeai/tests/snippets/analytics/get_average_cost_per_request.py +10 -0
- pygeai/tests/snippets/analytics/get_overall_error_rate.py +10 -0
- pygeai/tests/snippets/analytics/get_top_10_agents_by_requests.py +12 -0
- pygeai/tests/snippets/analytics/get_total_active_users.py +10 -0
- pygeai/tests/snippets/analytics/get_total_cost.py +10 -0
- pygeai/tests/snippets/analytics/get_total_requests_per_day.py +12 -0
- pygeai/tests/snippets/analytics/get_total_tokens.py +12 -0
- pygeai/tests/snippets/chat/get_response_complete_example.py +67 -0
- pygeai/tests/snippets/chat/get_response_with_instructions.py +19 -0
- pygeai/tests/snippets/chat/get_response_with_metadata.py +24 -0
- pygeai/tests/snippets/chat/get_response_with_parallel_tools.py +58 -0
- pygeai/tests/snippets/chat/get_response_with_reasoning.py +21 -0
- pygeai/tests/snippets/chat/get_response_with_store.py +38 -0
- pygeai/tests/snippets/chat/get_response_with_truncation.py +24 -0
- pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +39 -0
- pygeai/tests/snippets/lab/agents/create_agent_with_properties.py +46 -0
- pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +62 -0
- pygeai/tests/snippets/lab/agents/update_agent_properties.py +50 -0
- pygeai/tests/snippets/organization/add_project_member.py +10 -0
- pygeai/tests/snippets/organization/add_project_member_batch.py +44 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/METADATA +1 -1
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/RECORD +102 -92
- pygeai/_docs/source/pygeai.tests.snippets.assistants.data_analyst.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.assistants.rag.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.assistants.rst +0 -78
- pygeai/_docs/source/pygeai.tests.snippets.auth.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.chat.rst +0 -125
- pygeai/_docs/source/pygeai.tests.snippets.dbg.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.embeddings.rst +0 -61
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.dataset.rst +0 -197
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.plan.rst +0 -133
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.result.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.rst +0 -20
- pygeai/_docs/source/pygeai.tests.snippets.extras.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.files.rst +0 -53
- pygeai/_docs/source/pygeai.tests.snippets.gam.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.agents.rst +0 -93
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.jobs.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.kbs.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.rst +0 -46
- pygeai/_docs/source/pygeai.tests.snippets.lab.rst +0 -82
- pygeai/_docs/source/pygeai.tests.snippets.lab.samples.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.strategies.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.lab.tools.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.lab.use_cases.rst +0 -117
- pygeai/_docs/source/pygeai.tests.snippets.migrate.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.organization.rst +0 -109
- pygeai/_docs/source/pygeai.tests.snippets.rag.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.rerank.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.rst +0 -32
- pygeai/_docs/source/pygeai.tests.snippets.secrets.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.usage_limit.rst +0 -77
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/WHEEL +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/entry_points.txt +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/licenses/LICENSE +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b12.dist-info}/top_level.txt +0 -0
pygeai/cli/commands/migrate.py
CHANGED
|
@@ -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-
|
|
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 >
|
|
238
|
-
Console.write_stdout("Error: Invalid selection. Numbers must be between 1 and
|
|
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
|
|
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
|
+
)
|
pygeai/cli/error_handler.py
CHANGED
|
@@ -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(
|
|
23
|
-
|
|
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(
|
|
35
|
-
|
|
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[:
|
|
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]:
|