flowyml 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/dataset.py +570 -17
  3. flowyml/assets/metrics.py +5 -0
  4. flowyml/assets/model.py +1052 -15
  5. flowyml/cli/main.py +709 -0
  6. flowyml/cli/stack_cli.py +138 -25
  7. flowyml/core/__init__.py +17 -0
  8. flowyml/core/executor.py +231 -37
  9. flowyml/core/image_builder.py +129 -0
  10. flowyml/core/log_streamer.py +227 -0
  11. flowyml/core/orchestrator.py +59 -4
  12. flowyml/core/pipeline.py +65 -13
  13. flowyml/core/routing.py +558 -0
  14. flowyml/core/scheduler.py +88 -5
  15. flowyml/core/step.py +9 -1
  16. flowyml/core/step_grouping.py +49 -35
  17. flowyml/core/types.py +407 -0
  18. flowyml/integrations/keras.py +247 -82
  19. flowyml/monitoring/alerts.py +10 -0
  20. flowyml/monitoring/notifications.py +104 -25
  21. flowyml/monitoring/slack_blocks.py +323 -0
  22. flowyml/plugins/__init__.py +251 -0
  23. flowyml/plugins/alerters/__init__.py +1 -0
  24. flowyml/plugins/alerters/slack.py +168 -0
  25. flowyml/plugins/base.py +752 -0
  26. flowyml/plugins/config.py +478 -0
  27. flowyml/plugins/deployers/__init__.py +22 -0
  28. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  29. flowyml/plugins/deployers/sagemaker.py +306 -0
  30. flowyml/plugins/deployers/vertex.py +290 -0
  31. flowyml/plugins/integration.py +369 -0
  32. flowyml/plugins/manager.py +510 -0
  33. flowyml/plugins/model_registries/__init__.py +22 -0
  34. flowyml/plugins/model_registries/mlflow.py +159 -0
  35. flowyml/plugins/model_registries/sagemaker.py +489 -0
  36. flowyml/plugins/model_registries/vertex.py +386 -0
  37. flowyml/plugins/orchestrators/__init__.py +13 -0
  38. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  39. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  40. flowyml/plugins/registries/__init__.py +13 -0
  41. flowyml/plugins/registries/ecr.py +321 -0
  42. flowyml/plugins/registries/gcr.py +313 -0
  43. flowyml/plugins/registry.py +454 -0
  44. flowyml/plugins/stack.py +494 -0
  45. flowyml/plugins/stack_config.py +537 -0
  46. flowyml/plugins/stores/__init__.py +13 -0
  47. flowyml/plugins/stores/gcs.py +460 -0
  48. flowyml/plugins/stores/s3.py +453 -0
  49. flowyml/plugins/trackers/__init__.py +11 -0
  50. flowyml/plugins/trackers/mlflow.py +316 -0
  51. flowyml/plugins/validators/__init__.py +3 -0
  52. flowyml/plugins/validators/deepchecks.py +119 -0
  53. flowyml/registry/__init__.py +2 -1
  54. flowyml/registry/model_environment.py +109 -0
  55. flowyml/registry/model_registry.py +241 -96
  56. flowyml/serving/__init__.py +17 -0
  57. flowyml/serving/model_server.py +628 -0
  58. flowyml/stacks/__init__.py +60 -0
  59. flowyml/stacks/aws.py +93 -0
  60. flowyml/stacks/base.py +62 -0
  61. flowyml/stacks/components.py +12 -0
  62. flowyml/stacks/gcp.py +44 -9
  63. flowyml/stacks/plugins.py +115 -0
  64. flowyml/stacks/registry.py +2 -1
  65. flowyml/storage/sql.py +401 -12
  66. flowyml/tracking/experiment.py +8 -5
  67. flowyml/ui/backend/Dockerfile +87 -16
  68. flowyml/ui/backend/auth.py +12 -2
  69. flowyml/ui/backend/main.py +149 -5
  70. flowyml/ui/backend/routers/ai_context.py +226 -0
  71. flowyml/ui/backend/routers/assets.py +23 -4
  72. flowyml/ui/backend/routers/auth.py +96 -0
  73. flowyml/ui/backend/routers/deployments.py +660 -0
  74. flowyml/ui/backend/routers/model_explorer.py +597 -0
  75. flowyml/ui/backend/routers/plugins.py +103 -51
  76. flowyml/ui/backend/routers/projects.py +91 -8
  77. flowyml/ui/backend/routers/runs.py +132 -1
  78. flowyml/ui/backend/routers/schedules.py +54 -29
  79. flowyml/ui/backend/routers/templates.py +319 -0
  80. flowyml/ui/backend/routers/websocket.py +2 -2
  81. flowyml/ui/frontend/Dockerfile +55 -6
  82. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  83. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  84. flowyml/ui/frontend/dist/index.html +2 -2
  85. flowyml/ui/frontend/dist/logo.png +0 -0
  86. flowyml/ui/frontend/nginx.conf +65 -4
  87. flowyml/ui/frontend/package-lock.json +1415 -74
  88. flowyml/ui/frontend/package.json +4 -0
  89. flowyml/ui/frontend/public/logo.png +0 -0
  90. flowyml/ui/frontend/src/App.jsx +10 -7
  91. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  92. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  93. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  94. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  95. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  96. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  97. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  98. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  99. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
  100. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  101. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  102. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  103. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
  104. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  105. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  106. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  107. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  108. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  109. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  110. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  111. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  112. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  113. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  114. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  115. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  116. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  117. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  118. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  119. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  120. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  121. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  122. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  123. flowyml/ui/frontend/src/router/index.jsx +47 -20
  124. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  125. flowyml/ui/server_manager.py +5 -5
  126. flowyml/ui/utils.py +157 -39
  127. flowyml/utils/config.py +37 -15
  128. flowyml/utils/model_introspection.py +123 -0
  129. flowyml/utils/observability.py +30 -0
  130. flowyml-1.8.0.dist-info/METADATA +174 -0
  131. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
  132. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  133. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  134. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  135. flowyml-1.7.1.dist-info/METADATA +0 -477
  136. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  137. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
flowyml/cli/main.py CHANGED
@@ -376,6 +376,111 @@ def models() -> None:
376
376
  pass
377
377
 
378
378
 
379
+ @cli.group()
380
+ def db() -> None:
381
+ """Database management commands."""
382
+ pass
383
+
384
+
385
+ @db.command("migrate")
386
+ @click.option("--revision", default="head", help="Target revision (default: head)")
387
+ @click.option("--sql", is_flag=True, help="Generate SQL instead of running migration")
388
+ def migrate(revision: str, sql: bool) -> None:
389
+ """Run database migrations.
390
+
391
+ This applies Alembic migrations to your database schema.
392
+
393
+ Examples:
394
+ flowyml db migrate # Upgrade to latest
395
+ flowyml db migrate --revision 001_initial
396
+ flowyml db migrate --sql # Print SQL without executing
397
+ """
398
+ import os
399
+
400
+ db_url = os.getenv("FLOWYML_DATABASE_URL", "sqlite:///flowyml.db")
401
+ click.echo("🔄 Running database migrations...")
402
+ click.echo(f" Database: {db_url[:50]}...")
403
+
404
+ try:
405
+ from alembic.config import Config
406
+ from alembic import command
407
+
408
+ # Get alembic.ini path (project root)
409
+ alembic_cfg = Config("alembic.ini")
410
+ alembic_cfg.set_main_option("sqlalchemy.url", db_url)
411
+
412
+ if sql:
413
+ command.upgrade(alembic_cfg, revision, sql=True)
414
+ else:
415
+ command.upgrade(alembic_cfg, revision)
416
+ click.echo(f"✅ Migrations applied successfully to revision: {revision}")
417
+ except Exception as e:
418
+ click.echo(f"❌ Migration failed: {e}", err=True)
419
+ raise click.Abort()
420
+
421
+
422
+ @db.command("downgrade")
423
+ @click.argument("revision", default="-1")
424
+ def downgrade(revision: str) -> None:
425
+ """Downgrade database schema.
426
+
427
+ Examples:
428
+ flowyml db downgrade -1 # Downgrade one revision
429
+ flowyml db downgrade base # Downgrade to empty database
430
+ """
431
+ import os
432
+
433
+ db_url = os.getenv("FLOWYML_DATABASE_URL", "sqlite:///flowyml.db")
434
+ click.echo("🔄 Downgrading database...")
435
+
436
+ try:
437
+ from alembic.config import Config
438
+ from alembic import command
439
+
440
+ alembic_cfg = Config("alembic.ini")
441
+ alembic_cfg.set_main_option("sqlalchemy.url", db_url)
442
+
443
+ command.downgrade(alembic_cfg, revision)
444
+ click.echo(f"✅ Downgraded to revision: {revision}")
445
+ except Exception as e:
446
+ click.echo(f"❌ Downgrade failed: {e}", err=True)
447
+ raise click.Abort()
448
+
449
+
450
+ @db.command("current")
451
+ def current() -> None:
452
+ """Show current database revision."""
453
+ import os
454
+
455
+ db_url = os.getenv("FLOWYML_DATABASE_URL", "sqlite:///flowyml.db")
456
+
457
+ try:
458
+ from alembic.config import Config
459
+ from alembic import command
460
+
461
+ alembic_cfg = Config("alembic.ini")
462
+ alembic_cfg.set_main_option("sqlalchemy.url", db_url)
463
+
464
+ click.echo("Current database revision:")
465
+ command.current(alembic_cfg, verbose=True)
466
+ except Exception as e:
467
+ click.echo(f"❌ Failed to get current revision: {e}", err=True)
468
+
469
+
470
+ @db.command("history")
471
+ def history() -> None:
472
+ """Show migration history."""
473
+ try:
474
+ from alembic.config import Config
475
+ from alembic import command
476
+
477
+ alembic_cfg = Config("alembic.ini")
478
+ click.echo("Migration history:")
479
+ command.history(alembic_cfg, verbose=True)
480
+ except Exception as e:
481
+ click.echo(f"❌ Failed to get history: {e}", err=True)
482
+
483
+
379
484
  # Register model commands
380
485
  models.add_command(list_models)
381
486
  models.add_command(promote_model)
@@ -433,6 +538,18 @@ def set_url(server: str, ui: str) -> None:
433
538
  click.echo(f"✓ Remote UI URL set to '{ui}'")
434
539
  cfg.save()
435
540
 
541
+ cfg.save()
542
+
543
+
544
+ @config.command("set-token")
545
+ @click.argument("token")
546
+ def set_token(token: str) -> None:
547
+ """Set the API token for remote authentication."""
548
+ cfg = get_config()
549
+ cfg.api_token = token
550
+ cfg.save()
551
+ click.echo(f"✓ API token set (length: {len(token)})")
552
+
436
553
 
437
554
  @cli.command()
438
555
  @click.argument("run_id")
@@ -826,5 +943,597 @@ def server_status(host: str, port: int) -> None:
826
943
  click.echo("Start with: flowyml go")
827
944
 
828
945
 
946
+ # ============================================================================
947
+ # ZenML Integration Commands
948
+ # ============================================================================
949
+
950
+
951
+ @cli.group()
952
+ def zenml() -> None:
953
+ """ZenML integration commands - Seamlessly use ZenML components in FlowyML.
954
+
955
+ FlowyML can automatically discover and wrap ZenML integrations,
956
+ making them available as first-class FlowyML stack components.
957
+ """
958
+ pass
959
+
960
+
961
+ @zenml.command("list")
962
+ @click.option("--installed", is_flag=True, help="Show only installed integrations")
963
+ def list_zenml_integrations(installed: bool) -> None:
964
+ """List available ZenML integrations.
965
+
966
+ Shows all ZenML integrations that can be used with FlowyML.
967
+ Use --installed to see only the integrations you have installed.
968
+ """
969
+ from flowyml.stacks.plugins import get_component_registry
970
+
971
+ registry = get_component_registry()
972
+
973
+ if installed:
974
+ integrations = registry.list_installed_zenml_integrations()
975
+ click.echo("Installed ZenML integrations:\n")
976
+ else:
977
+ integrations = registry.list_zenml_integrations()
978
+ click.echo("Available ZenML integrations:\n")
979
+
980
+ if not integrations:
981
+ click.echo(" No integrations found.")
982
+ click.echo("\n Make sure ZenML is installed: pip install zenml")
983
+ return
984
+
985
+ for name in sorted(integrations):
986
+ click.echo(f" • {name}")
987
+
988
+ click.echo(f"\nTotal: {len(integrations)} integrations")
989
+
990
+ if not installed:
991
+ click.echo("\nTo install an integration:")
992
+ click.echo(" flowyml zenml install <integration_name>")
993
+
994
+
995
+ @zenml.command("install")
996
+ @click.argument("integration_name")
997
+ def install_zenml_integration(integration_name: str) -> None:
998
+ """Install a ZenML integration and its dependencies.
999
+
1000
+ This installs the ZenML integration package and all required
1001
+ dependencies, making it available for use in FlowyML pipelines.
1002
+
1003
+ Examples:
1004
+ flowyml zenml install mlflow
1005
+ flowyml zenml install kubernetes
1006
+ flowyml zenml install aws
1007
+ """
1008
+ from flowyml.stacks.plugins import get_component_registry
1009
+
1010
+ click.echo(f"Installing ZenML integration '{integration_name}'...")
1011
+
1012
+ registry = get_component_registry()
1013
+ success = registry.install_zenml_integration(integration_name)
1014
+
1015
+ if success:
1016
+ click.echo(f"✓ Successfully installed '{integration_name}'")
1017
+ click.echo("\nTo use this integration in FlowyML:")
1018
+ click.echo(f" flowyml zenml import {integration_name}")
1019
+ else:
1020
+ click.echo(f"✗ Failed to install '{integration_name}'", err=True)
1021
+ click.echo(" Check that ZenML is installed and the integration name is correct.")
1022
+
1023
+
1024
+ @zenml.command("import")
1025
+ @click.argument("integration_name")
1026
+ def import_zenml_integration(integration_name: str) -> None:
1027
+ """Import components from a ZenML integration.
1028
+
1029
+ Discovers all flavors provided by a ZenML integration and registers
1030
+ them as FlowyML stack components, ready to use in your pipelines.
1031
+
1032
+ Examples:
1033
+ flowyml zenml import mlflow
1034
+ flowyml zenml import kubernetes
1035
+ """
1036
+ from flowyml.stacks.plugins import get_component_registry
1037
+
1038
+ click.echo(f"Importing ZenML integration '{integration_name}'...")
1039
+
1040
+ registry = get_component_registry()
1041
+ components = registry.import_zenml_integration(integration_name)
1042
+
1043
+ if components:
1044
+ click.echo(f"✓ Successfully imported {len(components)} components:\n")
1045
+ for comp in components:
1046
+ click.echo(f" • {comp.__name__}")
1047
+ click.echo("\nThese components are now available in your FlowyML stacks.")
1048
+ else:
1049
+ click.echo(f"✗ No components imported from '{integration_name}'", err=True)
1050
+ click.echo(" Make sure the integration is installed:")
1051
+ click.echo(f" flowyml zenml install {integration_name}")
1052
+
1053
+
1054
+ @zenml.command("import-all")
1055
+ def import_all_zenml_integrations() -> None:
1056
+ """Import all components from all installed ZenML integrations.
1057
+
1058
+ This is the easiest way to make all ZenML components available
1059
+ in FlowyML with a single command.
1060
+
1061
+ Example:
1062
+ flowyml zenml import-all
1063
+ """
1064
+ from flowyml.stacks.plugins import get_component_registry
1065
+
1066
+ click.echo("Importing all installed ZenML integrations...")
1067
+
1068
+ registry = get_component_registry()
1069
+ result = registry.import_all_zenml()
1070
+
1071
+ if result:
1072
+ total = sum(len(comps) for comps in result.values())
1073
+ click.echo(f"✓ Successfully imported {total} components from {len(result)} integrations:\n")
1074
+
1075
+ for integration_name, components in result.items():
1076
+ click.echo(f" {integration_name}:")
1077
+ for comp in components:
1078
+ click.echo(f" • {comp.__name__}")
1079
+
1080
+ click.echo("\nAll components are now available in your FlowyML stacks.")
1081
+ else:
1082
+ click.echo("✗ No components imported", err=True)
1083
+ click.echo(" Make sure ZenML is installed and you have some integrations installed:")
1084
+ click.echo(" pip install zenml")
1085
+ click.echo(" flowyml zenml install mlflow")
1086
+
1087
+
1088
+ @zenml.command("status")
1089
+ def zenml_status() -> None:
1090
+ """Check ZenML availability and show integration summary.
1091
+
1092
+ Shows whether ZenML is installed and a summary of available
1093
+ and installed integrations.
1094
+ """
1095
+ try:
1096
+ import zenml
1097
+
1098
+ zenml_version = zenml.__version__
1099
+ zenml_available = True
1100
+ except ImportError:
1101
+ zenml_available = False
1102
+ zenml_version = None
1103
+
1104
+ if zenml_available:
1105
+ click.echo(f"✓ ZenML is installed (version {zenml_version})\n")
1106
+
1107
+ from flowyml.stacks.plugins import get_component_registry
1108
+
1109
+ registry = get_component_registry()
1110
+
1111
+ available = registry.list_zenml_integrations()
1112
+ installed = registry.list_installed_zenml_integrations()
1113
+
1114
+ click.echo(f" Available integrations: {len(available)}")
1115
+ click.echo(f" Installed integrations: {len(installed)}")
1116
+
1117
+ if installed:
1118
+ click.echo(f"\n Installed: {', '.join(installed[:5])}")
1119
+ if len(installed) > 5:
1120
+ click.echo(f" ...and {len(installed) - 5} more")
1121
+
1122
+ click.echo("\n Quick start:")
1123
+ click.echo(" flowyml zenml import-all # Import all installed integrations")
1124
+ else:
1125
+ click.echo("✗ ZenML is not installed\n")
1126
+ click.echo(" To install ZenML:")
1127
+ click.echo(" pip install zenml")
1128
+ click.echo("\n After installing, you can:")
1129
+ click.echo(" flowyml zenml list # List available integrations")
1130
+ click.echo(" flowyml zenml install aws # Install an integration")
1131
+ click.echo(" flowyml zenml import-all # Import all components")
1132
+
1133
+
1134
+ # =============================================================================
1135
+ # NATIVE PLUGIN COMMANDS
1136
+ # =============================================================================
1137
+
1138
+
1139
+ @cli.group()
1140
+ def plugin() -> None:
1141
+ """Native plugin management commands.
1142
+
1143
+ Manage FlowyML plugins without external framework dependencies.
1144
+ Install plugins directly (e.g., 'flowyml plugin install mlflow')
1145
+ and FlowyML will install only the underlying packages you need.
1146
+ """
1147
+ pass
1148
+
1149
+
1150
+ @plugin.command("list")
1151
+ @click.option("--installed", is_flag=True, help="Show only installed plugins")
1152
+ @click.option(
1153
+ "--type",
1154
+ "plugin_type",
1155
+ type=click.Choice(
1156
+ [
1157
+ "experiment_tracker",
1158
+ "artifact_store",
1159
+ "orchestrator",
1160
+ "container_registry",
1161
+ "feature_store",
1162
+ "data_validator",
1163
+ "alerter",
1164
+ ],
1165
+ ),
1166
+ help="Filter by plugin type",
1167
+ )
1168
+ def plugin_list(installed: bool, plugin_type: str) -> None:
1169
+ """List available plugins.
1170
+
1171
+ Shows all plugins in the FlowyML catalog. Use --installed to see
1172
+ only plugins whose packages are already installed.
1173
+
1174
+ Examples:
1175
+ flowyml plugin list
1176
+ flowyml plugin list --installed
1177
+ flowyml plugin list --type experiment_tracker
1178
+ """
1179
+ from flowyml.plugins import get_manager, PluginType
1180
+
1181
+ manager = get_manager()
1182
+
1183
+ # Convert string to PluginType if provided
1184
+ ptype = PluginType(plugin_type) if plugin_type else None
1185
+
1186
+ if installed:
1187
+ plugins = manager.list_installed(ptype)
1188
+ title = "Installed Plugins"
1189
+ else:
1190
+ plugins = manager.list_available(ptype)
1191
+ title = "Available Plugins"
1192
+
1193
+ if not plugins:
1194
+ click.echo("No plugins found.")
1195
+ return
1196
+
1197
+ click.echo(f"\n📦 {title}:\n")
1198
+
1199
+ # Group by type for better display
1200
+ from flowyml.plugins import get_plugin_info
1201
+
1202
+ grouped = {}
1203
+ for name in plugins:
1204
+ info = get_plugin_info(name)
1205
+ if info:
1206
+ type_name = info.plugin_type.value.replace("_", " ").title()
1207
+ if type_name not in grouped:
1208
+ grouped[type_name] = []
1209
+ is_installed = manager.is_installed(name)
1210
+ status = "✓" if is_installed else " "
1211
+ grouped[type_name].append((name, info.description, status))
1212
+
1213
+ for type_name, items in sorted(grouped.items()):
1214
+ click.echo(f" {type_name}:")
1215
+ for name, desc, status in sorted(items):
1216
+ click.echo(f" {status} {name:<20} - {desc[:50]}")
1217
+ click.echo()
1218
+
1219
+ click.echo("Install a plugin with: flowyml plugin install <name>")
1220
+
1221
+
1222
+ @plugin.command("install")
1223
+ @click.argument("name")
1224
+ @click.option("--upgrade", is_flag=True, help="Upgrade to latest version")
1225
+ def plugin_install(name: str, upgrade: bool) -> None:
1226
+ """Install a plugin.
1227
+
1228
+ Installs the underlying packages for a plugin directly.
1229
+ For example, 'flowyml plugin install mlflow' installs the mlflow package.
1230
+
1231
+ Examples:
1232
+ flowyml plugin install mlflow
1233
+ flowyml plugin install kubernetes
1234
+ flowyml plugin install s3 --upgrade
1235
+ """
1236
+ from flowyml.plugins import get_manager, get_plugin_info
1237
+
1238
+ manager = get_manager()
1239
+ info = get_plugin_info(name)
1240
+
1241
+ if not info:
1242
+ click.echo(f"✗ Plugin '{name}' not found", err=True)
1243
+ click.echo("\nAvailable plugins:")
1244
+ for p in manager.list_available()[:10]:
1245
+ click.echo(f" • {p}")
1246
+ if len(manager.list_available()) > 10:
1247
+ click.echo(f" ... and {len(manager.list_available()) - 10} more")
1248
+ return
1249
+
1250
+ click.echo(f"Installing plugin '{name}'...")
1251
+ click.echo(f" Packages: {', '.join(info.packages)}")
1252
+
1253
+ if manager.install(name, upgrade=upgrade):
1254
+ click.echo(f"\n✓ Plugin '{name}' installed successfully!")
1255
+ click.echo("\nUsage:")
1256
+ click.echo(" from flowyml.plugins import get_plugin")
1257
+ click.echo(f' plugin = get_plugin("{name}")')
1258
+ else:
1259
+ click.echo(f"\n✗ Failed to install '{name}'", err=True)
1260
+
1261
+
1262
+ @plugin.command("info")
1263
+ @click.argument("name")
1264
+ def plugin_info(name: str) -> None:
1265
+ """Show detailed information about a plugin.
1266
+
1267
+ Displays the plugin description, required packages, documentation URL,
1268
+ and current installation status.
1269
+ """
1270
+ from flowyml.plugins import get_plugin_info, get_manager
1271
+
1272
+ info = get_plugin_info(name)
1273
+ manager = get_manager()
1274
+
1275
+ if not info:
1276
+ click.echo(f"✗ Plugin '{name}' not found", err=True)
1277
+ return
1278
+
1279
+ is_installed = manager.is_installed(name)
1280
+ status = "✓ Installed" if is_installed else "○ Not installed"
1281
+
1282
+ click.echo(f"\n📦 Plugin: {info.name}")
1283
+ click.echo(f" Status: {status}")
1284
+ click.echo(f" Type: {info.plugin_type.value.replace('_', ' ').title()}")
1285
+ click.echo(f" Description: {info.description}")
1286
+ click.echo(f" Version: {info.version}")
1287
+ click.echo(f" Author: {info.author}")
1288
+ click.echo("\n Packages:")
1289
+ for pkg in info.packages:
1290
+ click.echo(f" • {pkg}")
1291
+ if info.tags:
1292
+ click.echo(f"\n Tags: {', '.join(info.tags)}")
1293
+ if info.documentation_url:
1294
+ click.echo(f"\n Docs: {info.documentation_url}")
1295
+
1296
+ if not is_installed:
1297
+ click.echo(f"\n Install with: flowyml plugin install {name}")
1298
+
1299
+
1300
+ @plugin.command("uninstall")
1301
+ @click.argument("name")
1302
+ @click.confirmation_option(prompt="Are you sure you want to uninstall?")
1303
+ def plugin_uninstall(name: str) -> None:
1304
+ """Uninstall a plugin.
1305
+
1306
+ Removes the underlying packages for a plugin.
1307
+ """
1308
+ from flowyml.plugins import get_manager
1309
+
1310
+ manager = get_manager()
1311
+
1312
+ if not manager.is_installed(name):
1313
+ click.echo(f"Plugin '{name}' is not installed")
1314
+ return
1315
+
1316
+ click.echo(f"Uninstalling plugin '{name}'...")
1317
+
1318
+ if manager.uninstall(name):
1319
+ click.echo(f"✓ Plugin '{name}' uninstalled")
1320
+ else:
1321
+ click.echo(f"✗ Failed to uninstall '{name}'", err=True)
1322
+
1323
+
1324
+ @plugin.command("install-git")
1325
+ @click.argument("git_url")
1326
+ def plugin_install_git(git_url: str) -> None:
1327
+ """Install a community plugin from a git repository.
1328
+
1329
+ Example:
1330
+ flowyml plugin install-git https://github.com/user/flowyml-custom-plugin.git
1331
+ """
1332
+ from flowyml.plugins import get_manager
1333
+
1334
+ manager = get_manager()
1335
+
1336
+ click.echo(f"Installing plugin from {git_url}...")
1337
+
1338
+ if manager.install_from_git(git_url):
1339
+ click.echo("✓ Plugin installed from git!")
1340
+ click.echo(" Run 'flowyml plugin list --installed' to see available plugins")
1341
+ else:
1342
+ click.echo("✗ Failed to install from git", err=True)
1343
+
1344
+
1345
+ # =============================================================================
1346
+ # STACK COMMANDS (Config-based plugin management)
1347
+ # =============================================================================
1348
+
1349
+
1350
+ @cli.group()
1351
+ def stack() -> None:
1352
+ """Stack configuration commands.
1353
+
1354
+ Configure your FlowyML stack (experiment_tracker, artifact_store,
1355
+ orchestrator, etc.) in flowyml.yaml for seamless integration.
1356
+
1357
+ With a configured stack, your code stays clean:
1358
+ from flowyml.plugins import start_run, log_metrics, save_model
1359
+
1360
+ start_run("training")
1361
+ log_metrics({"accuracy": 0.95})
1362
+ save_model(model, "models/classifier")
1363
+ """
1364
+ pass
1365
+
1366
+
1367
+ @stack.command("init")
1368
+ @click.option("--tracker", type=str, help="Experiment tracker plugin (e.g., mlflow)")
1369
+ @click.option("--store", type=str, help="Artifact store plugin (e.g., gcs, s3)")
1370
+ @click.option("--orchestrator", type=str, help="Orchestrator plugin (e.g., vertex_ai)")
1371
+ @click.option("--registry", type=str, help="Container registry plugin (e.g., gcr)")
1372
+ @click.option("--force", is_flag=True, help="Overwrite existing config")
1373
+ def stack_init(
1374
+ tracker: str,
1375
+ store: str,
1376
+ orchestrator: str,
1377
+ registry: str,
1378
+ force: bool,
1379
+ ) -> None:
1380
+ """Initialize a flowyml.yaml configuration file.
1381
+
1382
+ Creates a template configuration file with your selected plugins.
1383
+ You can then customize the configuration with your settings.
1384
+
1385
+ Examples:
1386
+ flowyml stack init --tracker mlflow --store gcs
1387
+ flowyml stack init --tracker mlflow --store s3 --orchestrator kubernetes
1388
+ """
1389
+ import os
1390
+ from flowyml.plugins import generate_config_template
1391
+
1392
+ config_path = "flowyml.yaml"
1393
+
1394
+ if os.path.exists(config_path) and not force:
1395
+ click.echo(f"✗ {config_path} already exists. Use --force to overwrite.", err=True)
1396
+ return
1397
+
1398
+ content = generate_config_template(
1399
+ tracker=tracker,
1400
+ store=store,
1401
+ orchestrator=orchestrator,
1402
+ registry=registry,
1403
+ )
1404
+
1405
+ with open(config_path, "w") as f:
1406
+ f.write(content)
1407
+
1408
+ click.echo(f"✓ Created {config_path}")
1409
+ click.echo("\nNext steps:")
1410
+ click.echo(" 1. Edit flowyml.yaml with your settings")
1411
+
1412
+ # Show which plugins to install
1413
+ plugins_to_install = [p for p in [tracker, store, orchestrator, registry] if p]
1414
+ if plugins_to_install:
1415
+ click.echo(f" 2. Install plugins: flowyml plugin install {' '.join(plugins_to_install)}")
1416
+
1417
+ click.echo(" 3. Use in code:")
1418
+ click.echo(" from flowyml.plugins import start_run, log_metrics, save_model")
1419
+ click.echo(' start_run("my_training")')
1420
+
1421
+
1422
+ @stack.command("show")
1423
+ def stack_show() -> None:
1424
+ """Show the currently configured stack.
1425
+
1426
+ Displays all plugins configured in flowyml.yaml and their status.
1427
+ """
1428
+ from flowyml.plugins import validate_stack, get_config
1429
+
1430
+ config = get_config()
1431
+ plugins_config = config.plugins_config
1432
+
1433
+ if not plugins_config:
1434
+ click.echo("No stack configured. Run 'flowyml stack init' to create flowyml.yaml")
1435
+ return
1436
+
1437
+ click.echo("\n📦 Current Stack:\n")
1438
+
1439
+ validation = validate_stack()
1440
+
1441
+ for role, conf in plugins_config.items():
1442
+ if isinstance(conf, dict):
1443
+ plugin_type = conf.get("type", "unknown")
1444
+ is_installed = validation.get(role, False)
1445
+ status = "✓" if is_installed else "○"
1446
+
1447
+ click.echo(f" {status} {role}:")
1448
+ click.echo(f" type: {plugin_type}")
1449
+
1450
+ # Show key config values (not sensitive ones)
1451
+ for key, value in conf.items():
1452
+ if key != "type" and "key" not in key.lower() and "secret" not in key.lower():
1453
+ click.echo(f" {key}: {value}")
1454
+ click.echo()
1455
+
1456
+ # Show missing plugins
1457
+ missing = [role for role, installed in validation.items() if not installed]
1458
+ if missing:
1459
+ click.echo("⚠️ Some plugins are not installed:")
1460
+ for role in missing:
1461
+ plugin_type = plugins_config.get(role, {}).get("type")
1462
+ if plugin_type:
1463
+ click.echo(f" flowyml plugin install {plugin_type}")
1464
+
1465
+
1466
+ @stack.command("validate")
1467
+ def stack_validate() -> None:
1468
+ """Validate the current stack configuration.
1469
+
1470
+ Checks that all configured plugins are installed and can be initialized.
1471
+ """
1472
+ from flowyml.plugins import get_config, validate_stack
1473
+
1474
+ click.echo("Validating stack configuration...\n")
1475
+
1476
+ config = get_config()
1477
+
1478
+ if not config._config_path:
1479
+ click.echo("✗ No flowyml.yaml found", err=True)
1480
+ click.echo(" Run 'flowyml stack init' to create one")
1481
+ return
1482
+
1483
+ click.echo(f"Config file: {config._config_path}\n")
1484
+
1485
+ validation = validate_stack()
1486
+
1487
+ if not validation:
1488
+ click.echo("No plugins configured")
1489
+ return
1490
+
1491
+ all_valid = True
1492
+ for role, is_installed in validation.items():
1493
+ status = "✓" if is_installed else "✗"
1494
+ click.echo(f" {status} {role}")
1495
+ if not is_installed:
1496
+ all_valid = False
1497
+
1498
+ click.echo()
1499
+ if all_valid:
1500
+ click.echo("✓ All plugins are installed and ready!")
1501
+ else:
1502
+ click.echo("✗ Some plugins need to be installed")
1503
+ click.echo(" Run 'flowyml stack show' for installation commands")
1504
+
1505
+
1506
+ @stack.command("install")
1507
+ def stack_install() -> None:
1508
+ """Install all plugins configured in flowyml.yaml."""
1509
+ from flowyml.plugins import get_config, get_manager
1510
+
1511
+ config = get_config()
1512
+ plugins_config = config.plugins_config
1513
+ manager = get_manager()
1514
+
1515
+ if not plugins_config:
1516
+ click.echo("No stack configured. Run 'flowyml stack init' first.")
1517
+ return
1518
+
1519
+ click.echo("Installing stack plugins...\n")
1520
+
1521
+ for _role, conf in plugins_config.items():
1522
+ if isinstance(conf, dict):
1523
+ plugin_type = conf.get("type")
1524
+ if plugin_type:
1525
+ if manager.is_installed(plugin_type):
1526
+ click.echo(f" ✓ {plugin_type} (already installed)")
1527
+ else:
1528
+ click.echo(f" Installing {plugin_type}...")
1529
+ if manager.install(plugin_type):
1530
+ click.echo(f" ✓ {plugin_type} installed")
1531
+ else:
1532
+ click.echo(f" ✗ Failed to install {plugin_type}")
1533
+
1534
+ click.echo("\n✓ Stack installation complete!")
1535
+ click.echo(" Run 'flowyml stack validate' to verify the configuration")
1536
+
1537
+
829
1538
  if __name__ == "__main__":
830
1539
  cli()