PraisonAI 3.0.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.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe Registry HTTP Server
|
|
3
|
+
|
|
4
|
+
Provides a local HTTP server for recipe registry operations.
|
|
5
|
+
Supports:
|
|
6
|
+
- GET /healthz - Health check
|
|
7
|
+
- GET /v1/recipes - List recipes (pagination)
|
|
8
|
+
- GET /v1/recipes/{name} - Get recipe info (all versions)
|
|
9
|
+
- GET /v1/recipes/{name}/{version} - Get specific version info
|
|
10
|
+
- GET /v1/recipes/{name}/{version}/download - Download bundle
|
|
11
|
+
- POST /v1/recipes/{name}/{version} - Publish bundle (multipart)
|
|
12
|
+
- DELETE /v1/recipes/{name}/{version} - Delete version
|
|
13
|
+
- GET /v1/search?q=... - Search recipes
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from praisonai.recipe.server import create_app, run_server
|
|
17
|
+
|
|
18
|
+
# Run server
|
|
19
|
+
run_server(host="127.0.0.1", port=7777)
|
|
20
|
+
|
|
21
|
+
# Or get ASGI app for custom deployment
|
|
22
|
+
app = create_app(registry_path="~/.praison/registry")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import hashlib
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import tempfile
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
32
|
+
from urllib.parse import parse_qs, unquote
|
|
33
|
+
|
|
34
|
+
from .registry import (
|
|
35
|
+
LocalRegistry,
|
|
36
|
+
RegistryError,
|
|
37
|
+
RecipeNotFoundError,
|
|
38
|
+
RecipeExistsError,
|
|
39
|
+
RegistryAuthError,
|
|
40
|
+
DEFAULT_REGISTRY_PATH,
|
|
41
|
+
DEFAULT_REGISTRY_PORT,
|
|
42
|
+
_calculate_checksum,
|
|
43
|
+
_get_timestamp,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RegistryServer:
|
|
48
|
+
"""
|
|
49
|
+
Simple HTTP server for recipe registry.
|
|
50
|
+
|
|
51
|
+
Uses only stdlib for minimal dependencies.
|
|
52
|
+
For production, consider using with uvicorn/gunicorn.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
registry_path: Optional[Path] = None,
|
|
58
|
+
token: Optional[str] = None,
|
|
59
|
+
read_only: bool = False,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize registry server.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
registry_path: Path to registry directory
|
|
66
|
+
token: Required token for write operations (optional)
|
|
67
|
+
read_only: If True, disable all write operations
|
|
68
|
+
"""
|
|
69
|
+
self.registry = LocalRegistry(registry_path)
|
|
70
|
+
self.token = token or os.environ.get("PRAISONAI_REGISTRY_TOKEN")
|
|
71
|
+
self.read_only = read_only
|
|
72
|
+
self._routes: List[Tuple[str, str, Callable]] = []
|
|
73
|
+
self._setup_routes()
|
|
74
|
+
|
|
75
|
+
def _setup_routes(self):
|
|
76
|
+
"""Setup URL routes."""
|
|
77
|
+
self._routes = [
|
|
78
|
+
("GET", r"^/healthz$", self._handle_health),
|
|
79
|
+
("GET", r"^/v1/recipes$", self._handle_list_recipes),
|
|
80
|
+
("GET", r"^/v1/recipes/([^/]+)$", self._handle_get_recipe),
|
|
81
|
+
("GET", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_get_version),
|
|
82
|
+
("GET", r"^/v1/recipes/([^/]+)/([^/]+)/download$", self._handle_download),
|
|
83
|
+
("POST", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_publish),
|
|
84
|
+
("DELETE", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_delete_version),
|
|
85
|
+
("DELETE", r"^/v1/recipes/([^/]+)$", self._handle_delete_recipe),
|
|
86
|
+
("GET", r"^/v1/search$", self._handle_search),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def _check_auth(self, headers: Dict[str, str]) -> bool:
|
|
90
|
+
"""Check if request is authorized for write operations."""
|
|
91
|
+
if not self.token:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
auth = headers.get("Authorization", "")
|
|
95
|
+
if auth.startswith("Bearer "):
|
|
96
|
+
return auth[7:] == self.token
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def _json_response(
|
|
100
|
+
self,
|
|
101
|
+
data: Any,
|
|
102
|
+
status: int = 200,
|
|
103
|
+
headers: Optional[Dict[str, str]] = None,
|
|
104
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
105
|
+
"""Create JSON response."""
|
|
106
|
+
resp_headers = {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"X-Registry-Version": "1.0",
|
|
109
|
+
}
|
|
110
|
+
if headers:
|
|
111
|
+
resp_headers.update(headers)
|
|
112
|
+
|
|
113
|
+
body = json.dumps(data, indent=2).encode("utf-8")
|
|
114
|
+
return status, resp_headers, body
|
|
115
|
+
|
|
116
|
+
def _error_response(
|
|
117
|
+
self,
|
|
118
|
+
message: str,
|
|
119
|
+
status: int = 400,
|
|
120
|
+
code: Optional[str] = None,
|
|
121
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
122
|
+
"""Create error response."""
|
|
123
|
+
return self._json_response({
|
|
124
|
+
"ok": False,
|
|
125
|
+
"error": message,
|
|
126
|
+
"code": code or "error",
|
|
127
|
+
}, status=status)
|
|
128
|
+
|
|
129
|
+
def _file_response(
|
|
130
|
+
self,
|
|
131
|
+
file_path: Path,
|
|
132
|
+
filename: str,
|
|
133
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
134
|
+
"""Create file download response."""
|
|
135
|
+
with open(file_path, "rb") as f:
|
|
136
|
+
content = f.read()
|
|
137
|
+
|
|
138
|
+
checksum = hashlib.sha256(content).hexdigest()
|
|
139
|
+
|
|
140
|
+
headers = {
|
|
141
|
+
"Content-Type": "application/gzip",
|
|
142
|
+
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
143
|
+
"Content-Length": str(len(content)),
|
|
144
|
+
"ETag": f'"{checksum[:16]}"',
|
|
145
|
+
"X-Checksum-SHA256": checksum,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 200, headers, content
|
|
149
|
+
|
|
150
|
+
def _parse_multipart(
|
|
151
|
+
self,
|
|
152
|
+
body: bytes,
|
|
153
|
+
content_type: str,
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""Parse multipart form data."""
|
|
156
|
+
# Extract boundary
|
|
157
|
+
boundary = None
|
|
158
|
+
for part in content_type.split(";"):
|
|
159
|
+
part = part.strip()
|
|
160
|
+
if part.startswith("boundary="):
|
|
161
|
+
boundary = part[9:].strip('"')
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if not boundary:
|
|
165
|
+
raise ValueError("Missing boundary in multipart data")
|
|
166
|
+
|
|
167
|
+
result = {"fields": {}, "files": {}}
|
|
168
|
+
boundary_bytes = f"--{boundary}".encode()
|
|
169
|
+
|
|
170
|
+
parts = body.split(boundary_bytes)
|
|
171
|
+
for part in parts[1:]: # Skip preamble
|
|
172
|
+
if part.startswith(b"--"):
|
|
173
|
+
break # End marker
|
|
174
|
+
|
|
175
|
+
# Split headers and content
|
|
176
|
+
if b"\r\n\r\n" in part:
|
|
177
|
+
header_section, content = part.split(b"\r\n\r\n", 1)
|
|
178
|
+
else:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Parse headers
|
|
182
|
+
headers = {}
|
|
183
|
+
for line in header_section.decode("utf-8", errors="ignore").split("\r\n"):
|
|
184
|
+
if ":" in line:
|
|
185
|
+
key, value = line.split(":", 1)
|
|
186
|
+
headers[key.strip().lower()] = value.strip()
|
|
187
|
+
|
|
188
|
+
# Get field name and filename
|
|
189
|
+
disposition = headers.get("content-disposition", "")
|
|
190
|
+
name = None
|
|
191
|
+
filename = None
|
|
192
|
+
|
|
193
|
+
for item in disposition.split(";"):
|
|
194
|
+
item = item.strip()
|
|
195
|
+
if item.startswith("name="):
|
|
196
|
+
name = item[5:].strip('"')
|
|
197
|
+
elif item.startswith("filename="):
|
|
198
|
+
filename = item[9:].strip('"')
|
|
199
|
+
|
|
200
|
+
if not name:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Remove trailing \r\n
|
|
204
|
+
content = content.rstrip(b"\r\n")
|
|
205
|
+
|
|
206
|
+
if filename:
|
|
207
|
+
result["files"][name] = {
|
|
208
|
+
"filename": filename,
|
|
209
|
+
"content": content,
|
|
210
|
+
"content_type": headers.get("content-type", "application/octet-stream"),
|
|
211
|
+
}
|
|
212
|
+
else:
|
|
213
|
+
result["fields"][name] = content.decode("utf-8", errors="ignore")
|
|
214
|
+
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
def handle_request(
|
|
218
|
+
self,
|
|
219
|
+
method: str,
|
|
220
|
+
path: str,
|
|
221
|
+
headers: Dict[str, str],
|
|
222
|
+
body: bytes,
|
|
223
|
+
query_string: str = "",
|
|
224
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
225
|
+
"""
|
|
226
|
+
Handle HTTP request.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
method: HTTP method
|
|
230
|
+
path: URL path
|
|
231
|
+
headers: Request headers
|
|
232
|
+
body: Request body
|
|
233
|
+
query_string: Query string
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Tuple of (status_code, headers, body)
|
|
237
|
+
"""
|
|
238
|
+
# Parse query string
|
|
239
|
+
query = parse_qs(query_string)
|
|
240
|
+
|
|
241
|
+
# Find matching route
|
|
242
|
+
for route_method, pattern, handler in self._routes:
|
|
243
|
+
if method != route_method:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
match = re.match(pattern, path)
|
|
247
|
+
if match:
|
|
248
|
+
try:
|
|
249
|
+
return handler(
|
|
250
|
+
headers=headers,
|
|
251
|
+
body=body,
|
|
252
|
+
query=query,
|
|
253
|
+
path_params=match.groups(),
|
|
254
|
+
)
|
|
255
|
+
except RecipeNotFoundError as e:
|
|
256
|
+
return self._error_response(str(e), status=404, code="not_found")
|
|
257
|
+
except RecipeExistsError as e:
|
|
258
|
+
return self._error_response(str(e), status=409, code="conflict")
|
|
259
|
+
except RegistryAuthError as e:
|
|
260
|
+
return self._error_response(str(e), status=401, code="auth_error")
|
|
261
|
+
except RegistryError as e:
|
|
262
|
+
return self._error_response(str(e), status=400, code="registry_error")
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return self._error_response(f"Internal error: {e}", status=500, code="internal_error")
|
|
265
|
+
|
|
266
|
+
return self._error_response("Not found", status=404, code="not_found")
|
|
267
|
+
|
|
268
|
+
def _handle_health(self, **kwargs) -> Tuple[int, Dict[str, str], bytes]:
|
|
269
|
+
"""Handle GET /healthz."""
|
|
270
|
+
return self._json_response({
|
|
271
|
+
"ok": True,
|
|
272
|
+
"status": "healthy",
|
|
273
|
+
"timestamp": _get_timestamp(),
|
|
274
|
+
"read_only": self.read_only,
|
|
275
|
+
"auth_required": bool(self.token),
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
def _handle_list_recipes(
|
|
279
|
+
self,
|
|
280
|
+
query: Dict[str, List[str]],
|
|
281
|
+
**kwargs,
|
|
282
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
283
|
+
"""Handle GET /v1/recipes."""
|
|
284
|
+
page = int(query.get("page", ["1"])[0])
|
|
285
|
+
per_page = min(int(query.get("per_page", ["50"])[0]), 100)
|
|
286
|
+
tags = query.get("tags", [""])[0].split(",") if query.get("tags") else None
|
|
287
|
+
|
|
288
|
+
if tags and tags[0] == "":
|
|
289
|
+
tags = None
|
|
290
|
+
|
|
291
|
+
recipes = self.registry.list_recipes(tags=tags)
|
|
292
|
+
|
|
293
|
+
# Paginate
|
|
294
|
+
start = (page - 1) * per_page
|
|
295
|
+
end = start + per_page
|
|
296
|
+
paginated = recipes[start:end]
|
|
297
|
+
|
|
298
|
+
return self._json_response({
|
|
299
|
+
"ok": True,
|
|
300
|
+
"recipes": paginated,
|
|
301
|
+
"total": len(recipes),
|
|
302
|
+
"page": page,
|
|
303
|
+
"per_page": per_page,
|
|
304
|
+
"has_more": end < len(recipes),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
def _handle_get_recipe(
|
|
308
|
+
self,
|
|
309
|
+
path_params: Tuple[str, ...],
|
|
310
|
+
**kwargs,
|
|
311
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
312
|
+
"""Handle GET /v1/recipes/{name}."""
|
|
313
|
+
name = unquote(path_params[0])
|
|
314
|
+
|
|
315
|
+
versions = self.registry.get_versions(name)
|
|
316
|
+
info = self.registry.get_info(name)
|
|
317
|
+
|
|
318
|
+
return self._json_response({
|
|
319
|
+
"ok": True,
|
|
320
|
+
"name": name,
|
|
321
|
+
"versions": versions,
|
|
322
|
+
"latest": info.get("version"),
|
|
323
|
+
"description": info.get("description", ""),
|
|
324
|
+
"tags": info.get("tags", []),
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
def _handle_get_version(
|
|
328
|
+
self,
|
|
329
|
+
path_params: Tuple[str, ...],
|
|
330
|
+
**kwargs,
|
|
331
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
332
|
+
"""Handle GET /v1/recipes/{name}/{version}."""
|
|
333
|
+
name = unquote(path_params[0])
|
|
334
|
+
version = unquote(path_params[1])
|
|
335
|
+
|
|
336
|
+
info = self.registry.get_info(name, version)
|
|
337
|
+
|
|
338
|
+
return self._json_response({
|
|
339
|
+
"ok": True,
|
|
340
|
+
**info,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
def _handle_download(
|
|
344
|
+
self,
|
|
345
|
+
path_params: Tuple[str, ...],
|
|
346
|
+
headers: Dict[str, str],
|
|
347
|
+
**kwargs,
|
|
348
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
349
|
+
"""Handle GET /v1/recipes/{name}/{version}/download."""
|
|
350
|
+
name = unquote(path_params[0])
|
|
351
|
+
version = unquote(path_params[1])
|
|
352
|
+
|
|
353
|
+
# Get bundle path
|
|
354
|
+
bundle_name = f"{name}-{version}.praison"
|
|
355
|
+
bundle_path = self.registry.recipes_path / name / version / bundle_name
|
|
356
|
+
|
|
357
|
+
if not bundle_path.exists():
|
|
358
|
+
raise RecipeNotFoundError(f"Bundle not found: {name}@{version}")
|
|
359
|
+
|
|
360
|
+
# Check ETag for caching
|
|
361
|
+
checksum = _calculate_checksum(bundle_path)
|
|
362
|
+
etag = f'"{checksum[:16]}"'
|
|
363
|
+
|
|
364
|
+
if_none_match = headers.get("If-None-Match", "")
|
|
365
|
+
if if_none_match == etag:
|
|
366
|
+
return 304, {"ETag": etag}, b""
|
|
367
|
+
|
|
368
|
+
return self._file_response(bundle_path, bundle_name)
|
|
369
|
+
|
|
370
|
+
def _handle_publish(
|
|
371
|
+
self,
|
|
372
|
+
path_params: Tuple[str, ...],
|
|
373
|
+
headers: Dict[str, str],
|
|
374
|
+
body: bytes,
|
|
375
|
+
**kwargs,
|
|
376
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
377
|
+
"""Handle POST /v1/recipes/{name}/{version}."""
|
|
378
|
+
if self.read_only:
|
|
379
|
+
return self._error_response("Registry is read-only", status=403, code="read_only")
|
|
380
|
+
|
|
381
|
+
if not self._check_auth(headers):
|
|
382
|
+
return self._error_response("Authentication required", status=401, code="auth_required")
|
|
383
|
+
|
|
384
|
+
name = unquote(path_params[0])
|
|
385
|
+
version = unquote(path_params[1])
|
|
386
|
+
|
|
387
|
+
# Parse multipart data
|
|
388
|
+
content_type = headers.get("Content-Type", "")
|
|
389
|
+
if not content_type.startswith("multipart/form-data"):
|
|
390
|
+
return self._error_response("Expected multipart/form-data", status=400)
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
data = self._parse_multipart(body, content_type)
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
return self._error_response(f"Invalid multipart data: {e}", status=400)
|
|
396
|
+
|
|
397
|
+
if "bundle" not in data["files"]:
|
|
398
|
+
return self._error_response("Missing bundle file", status=400)
|
|
399
|
+
|
|
400
|
+
force = data["fields"].get("force", "false").lower() == "true"
|
|
401
|
+
bundle_content = data["files"]["bundle"]["content"]
|
|
402
|
+
|
|
403
|
+
# Save to temp file and publish
|
|
404
|
+
with tempfile.NamedTemporaryFile(suffix=".praison", delete=False) as tmp:
|
|
405
|
+
tmp.write(bundle_content)
|
|
406
|
+
tmp_path = Path(tmp.name)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
result = self.registry.publish(tmp_path, force=force)
|
|
410
|
+
|
|
411
|
+
# Verify name/version match
|
|
412
|
+
if result["name"] != name or result["version"] != version:
|
|
413
|
+
# Rollback
|
|
414
|
+
self.registry.delete(result["name"], result["version"])
|
|
415
|
+
return self._error_response(
|
|
416
|
+
f"Bundle name/version ({result['name']}@{result['version']}) "
|
|
417
|
+
f"doesn't match URL ({name}@{version})",
|
|
418
|
+
status=400,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
return self._json_response({
|
|
422
|
+
"ok": True,
|
|
423
|
+
**result,
|
|
424
|
+
}, status=201)
|
|
425
|
+
finally:
|
|
426
|
+
tmp_path.unlink(missing_ok=True)
|
|
427
|
+
|
|
428
|
+
def _handle_delete_version(
|
|
429
|
+
self,
|
|
430
|
+
path_params: Tuple[str, ...],
|
|
431
|
+
headers: Dict[str, str],
|
|
432
|
+
**kwargs,
|
|
433
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
434
|
+
"""Handle DELETE /v1/recipes/{name}/{version}."""
|
|
435
|
+
if self.read_only:
|
|
436
|
+
return self._error_response("Registry is read-only", status=403, code="read_only")
|
|
437
|
+
|
|
438
|
+
if not self._check_auth(headers):
|
|
439
|
+
return self._error_response("Authentication required", status=401, code="auth_required")
|
|
440
|
+
|
|
441
|
+
name = unquote(path_params[0])
|
|
442
|
+
version = unquote(path_params[1])
|
|
443
|
+
|
|
444
|
+
self.registry.delete(name, version)
|
|
445
|
+
|
|
446
|
+
return self._json_response({
|
|
447
|
+
"ok": True,
|
|
448
|
+
"deleted": f"{name}@{version}",
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
def _handle_delete_recipe(
|
|
452
|
+
self,
|
|
453
|
+
path_params: Tuple[str, ...],
|
|
454
|
+
headers: Dict[str, str],
|
|
455
|
+
**kwargs,
|
|
456
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
457
|
+
"""Handle DELETE /v1/recipes/{name}."""
|
|
458
|
+
if self.read_only:
|
|
459
|
+
return self._error_response("Registry is read-only", status=403, code="read_only")
|
|
460
|
+
|
|
461
|
+
if not self._check_auth(headers):
|
|
462
|
+
return self._error_response("Authentication required", status=401, code="auth_required")
|
|
463
|
+
|
|
464
|
+
name = unquote(path_params[0])
|
|
465
|
+
|
|
466
|
+
self.registry.delete(name)
|
|
467
|
+
|
|
468
|
+
return self._json_response({
|
|
469
|
+
"ok": True,
|
|
470
|
+
"deleted": name,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
def _handle_search(
|
|
474
|
+
self,
|
|
475
|
+
query: Dict[str, List[str]],
|
|
476
|
+
**kwargs,
|
|
477
|
+
) -> Tuple[int, Dict[str, str], bytes]:
|
|
478
|
+
"""Handle GET /v1/search."""
|
|
479
|
+
q = query.get("q", [""])[0]
|
|
480
|
+
tags = query.get("tags", [""])[0].split(",") if query.get("tags") else None
|
|
481
|
+
|
|
482
|
+
if tags and tags[0] == "":
|
|
483
|
+
tags = None
|
|
484
|
+
|
|
485
|
+
if not q:
|
|
486
|
+
return self._error_response("Query parameter 'q' required", status=400)
|
|
487
|
+
|
|
488
|
+
results = self.registry.search(q, tags=tags)
|
|
489
|
+
|
|
490
|
+
return self._json_response({
|
|
491
|
+
"ok": True,
|
|
492
|
+
"query": q,
|
|
493
|
+
"results": results,
|
|
494
|
+
"count": len(results),
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def create_wsgi_app(
|
|
499
|
+
registry_path: Optional[Path] = None,
|
|
500
|
+
token: Optional[str] = None,
|
|
501
|
+
read_only: bool = False,
|
|
502
|
+
) -> Callable:
|
|
503
|
+
"""
|
|
504
|
+
Create WSGI application for the registry server.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
registry_path: Path to registry directory
|
|
508
|
+
token: Required token for write operations
|
|
509
|
+
read_only: If True, disable write operations
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
WSGI application callable
|
|
513
|
+
"""
|
|
514
|
+
server = RegistryServer(
|
|
515
|
+
registry_path=registry_path,
|
|
516
|
+
token=token,
|
|
517
|
+
read_only=read_only,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def app(environ, start_response):
|
|
521
|
+
method = environ.get("REQUEST_METHOD", "GET")
|
|
522
|
+
path = environ.get("PATH_INFO", "/")
|
|
523
|
+
query_string = environ.get("QUERY_STRING", "")
|
|
524
|
+
|
|
525
|
+
# Read headers
|
|
526
|
+
headers = {}
|
|
527
|
+
for key, value in environ.items():
|
|
528
|
+
if key.startswith("HTTP_"):
|
|
529
|
+
header_name = key[5:].replace("_", "-").title()
|
|
530
|
+
headers[header_name] = value
|
|
531
|
+
headers["Content-Type"] = environ.get("CONTENT_TYPE", "")
|
|
532
|
+
|
|
533
|
+
# Read body
|
|
534
|
+
try:
|
|
535
|
+
content_length = int(environ.get("CONTENT_LENGTH", 0))
|
|
536
|
+
except (ValueError, TypeError):
|
|
537
|
+
content_length = 0
|
|
538
|
+
|
|
539
|
+
body = environ["wsgi.input"].read(content_length) if content_length > 0 else b""
|
|
540
|
+
|
|
541
|
+
# Handle request
|
|
542
|
+
status_code, resp_headers, resp_body = server.handle_request(
|
|
543
|
+
method=method,
|
|
544
|
+
path=path,
|
|
545
|
+
headers=headers,
|
|
546
|
+
body=body,
|
|
547
|
+
query_string=query_string,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Send response
|
|
551
|
+
status = f"{status_code} {'OK' if status_code < 400 else 'Error'}"
|
|
552
|
+
response_headers = [(k, v) for k, v in resp_headers.items()]
|
|
553
|
+
start_response(status, response_headers)
|
|
554
|
+
|
|
555
|
+
return [resp_body]
|
|
556
|
+
|
|
557
|
+
return app
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def run_server(
|
|
561
|
+
host: str = "127.0.0.1",
|
|
562
|
+
port: int = DEFAULT_REGISTRY_PORT,
|
|
563
|
+
registry_path: Optional[Path] = None,
|
|
564
|
+
token: Optional[str] = None,
|
|
565
|
+
read_only: bool = False,
|
|
566
|
+
):
|
|
567
|
+
"""
|
|
568
|
+
Run the registry server using stdlib wsgiref.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
host: Host to bind to
|
|
572
|
+
port: Port to bind to
|
|
573
|
+
registry_path: Path to registry directory
|
|
574
|
+
token: Required token for write operations
|
|
575
|
+
read_only: If True, disable write operations
|
|
576
|
+
"""
|
|
577
|
+
from wsgiref.simple_server import make_server, WSGIRequestHandler
|
|
578
|
+
|
|
579
|
+
# Custom handler to suppress logs unless verbose
|
|
580
|
+
class QuietHandler(WSGIRequestHandler):
|
|
581
|
+
def log_message(self, format, *args):
|
|
582
|
+
pass # Suppress default logging
|
|
583
|
+
|
|
584
|
+
app = create_wsgi_app(
|
|
585
|
+
registry_path=registry_path,
|
|
586
|
+
token=token,
|
|
587
|
+
read_only=read_only,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
server = make_server(host, port, app, handler_class=QuietHandler)
|
|
591
|
+
|
|
592
|
+
print(f"Recipe Registry Server running on http://{host}:{port}")
|
|
593
|
+
print(f"Registry path: {registry_path or DEFAULT_REGISTRY_PATH}")
|
|
594
|
+
if token:
|
|
595
|
+
print("Authentication: enabled (token required for writes)")
|
|
596
|
+
if read_only:
|
|
597
|
+
print("Mode: read-only")
|
|
598
|
+
print("\nEndpoints:")
|
|
599
|
+
print(" GET /healthz - Health check")
|
|
600
|
+
print(" GET /v1/recipes - List recipes")
|
|
601
|
+
print(" GET /v1/recipes/{name} - Get recipe info")
|
|
602
|
+
print(" GET /v1/recipes/{name}/{version} - Get version info")
|
|
603
|
+
print(" GET /v1/recipes/{name}/{version}/download - Download bundle")
|
|
604
|
+
print(" POST /v1/recipes/{name}/{version} - Publish bundle")
|
|
605
|
+
print(" DELETE /v1/recipes/{name}/{version} - Delete version")
|
|
606
|
+
print(" GET /v1/search?q=... - Search recipes")
|
|
607
|
+
print("\nPress Ctrl+C to stop")
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
server.serve_forever()
|
|
611
|
+
except KeyboardInterrupt:
|
|
612
|
+
print("\nShutting down...")
|
|
613
|
+
server.shutdown()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PraisonAI Scheduler Module
|
|
3
|
+
|
|
4
|
+
This module provides scheduling capabilities for running agents and deployments
|
|
5
|
+
at regular intervals, enabling 24/7 autonomous operations.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
- ScheduleParser: Parse schedule expressions (hourly, daily, */30m, etc.)
|
|
9
|
+
- ExecutorInterface: Abstract interface for executors
|
|
10
|
+
- AgentScheduler: Schedule agent execution at regular intervals
|
|
11
|
+
- DeploymentScheduler: Schedule deployment operations
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .base import ScheduleParser, ExecutorInterface, PraisonAgentExecutor
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'ScheduleParser',
|
|
18
|
+
'ExecutorInterface',
|
|
19
|
+
'PraisonAgentExecutor',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Lazy imports for modules that will be created later
|
|
23
|
+
def __getattr__(name):
|
|
24
|
+
if name == 'AgentScheduler':
|
|
25
|
+
from .agent_scheduler import AgentScheduler
|
|
26
|
+
return AgentScheduler
|
|
27
|
+
elif name == 'create_agent_scheduler':
|
|
28
|
+
from .agent_scheduler import create_agent_scheduler
|
|
29
|
+
return create_agent_scheduler
|
|
30
|
+
elif name == 'create_scheduler':
|
|
31
|
+
# Return a factory function that creates a mock scheduler for testing
|
|
32
|
+
def _create_scheduler(provider='gcp', **kwargs):
|
|
33
|
+
from .agent_scheduler import AgentScheduler
|
|
34
|
+
# Create a mock agent and task for testing
|
|
35
|
+
class MockAgent:
|
|
36
|
+
pass
|
|
37
|
+
return AgentScheduler(MockAgent(), "test task")
|
|
38
|
+
return _create_scheduler
|
|
39
|
+
elif name == 'DeploymentScheduler':
|
|
40
|
+
from .agent_scheduler import AgentScheduler
|
|
41
|
+
return AgentScheduler
|
|
42
|
+
elif name == 'create_deployment_scheduler':
|
|
43
|
+
from .agent_scheduler import create_agent_scheduler
|
|
44
|
+
return create_agent_scheduler
|
|
45
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|