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,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interfaces for StateStore.
|
|
3
|
+
|
|
4
|
+
StateStore handles fast key-value state and caching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StateStore(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for key-value state persistence.
|
|
15
|
+
|
|
16
|
+
Implementations handle fast state/cache storage for different backends:
|
|
17
|
+
- Redis (dedicated KV store)
|
|
18
|
+
- DynamoDB, Firestore (serverless document stores)
|
|
19
|
+
- MongoDB (document store)
|
|
20
|
+
- Upstash (serverless Redis)
|
|
21
|
+
- In-memory / JSON file (zero-dependency fallback)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get(self, key: str) -> Optional[Any]:
|
|
26
|
+
"""Get a value by key. Returns None if not found."""
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def set(
|
|
31
|
+
self,
|
|
32
|
+
key: str,
|
|
33
|
+
value: Any,
|
|
34
|
+
ttl: Optional[int] = None
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Set a value. TTL is in seconds."""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def delete(self, key: str) -> bool:
|
|
41
|
+
"""Delete a key. Returns True if key existed."""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def exists(self, key: str) -> bool:
|
|
46
|
+
"""Check if a key exists."""
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def keys(self, pattern: str = "*") -> List[str]:
|
|
51
|
+
"""List keys matching pattern."""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def ttl(self, key: str) -> Optional[int]:
|
|
56
|
+
"""Get remaining TTL in seconds. Returns None if no TTL or key doesn't exist."""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def expire(self, key: str, ttl: int) -> bool:
|
|
61
|
+
"""Set TTL on existing key. Returns True if key exists."""
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def get_json(self, key: str) -> Optional[Any]:
|
|
65
|
+
"""Get and deserialize JSON value."""
|
|
66
|
+
value = self.get(key)
|
|
67
|
+
if value is None:
|
|
68
|
+
return None
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
return json.loads(value)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def set_json(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
74
|
+
"""Serialize and set JSON value."""
|
|
75
|
+
self.set(key, json.dumps(value), ttl)
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def hget(self, key: str, field: str) -> Optional[Any]:
|
|
79
|
+
"""Get a field from a hash."""
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def hset(self, key: str, field: str, value: Any) -> None:
|
|
84
|
+
"""Set a field in a hash."""
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def hgetall(self, key: str) -> Dict[str, Any]:
|
|
89
|
+
"""Get all fields from a hash."""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def hdel(self, key: str, *fields: str) -> int:
|
|
94
|
+
"""Delete fields from a hash. Returns count deleted."""
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def close(self) -> None:
|
|
99
|
+
"""Close the store and release resources."""
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
def __enter__(self):
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
106
|
+
self.close()
|
|
107
|
+
return False
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DynamoDB implementation of StateStore.
|
|
3
|
+
|
|
4
|
+
Requires: boto3
|
|
5
|
+
Install: pip install boto3
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .base import StateStore
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DynamoDBStateStore(StateStore):
|
|
20
|
+
"""
|
|
21
|
+
DynamoDB-based state store.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
store = DynamoDBStateStore(
|
|
25
|
+
table_name="praisonai_state",
|
|
26
|
+
region="us-east-1"
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
table_name: str = "praisonai_state",
|
|
33
|
+
region: Optional[str] = None,
|
|
34
|
+
endpoint_url: Optional[str] = None,
|
|
35
|
+
auto_create_table: bool = True,
|
|
36
|
+
):
|
|
37
|
+
try:
|
|
38
|
+
import boto3
|
|
39
|
+
except ImportError:
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"boto3 is required for DynamoDB support. "
|
|
42
|
+
"Install with: pip install boto3"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
region = region or os.getenv("AWS_REGION", "us-east-1")
|
|
46
|
+
|
|
47
|
+
self._dynamodb = boto3.resource(
|
|
48
|
+
"dynamodb",
|
|
49
|
+
region_name=region,
|
|
50
|
+
endpoint_url=endpoint_url,
|
|
51
|
+
)
|
|
52
|
+
self._client = boto3.client(
|
|
53
|
+
"dynamodb",
|
|
54
|
+
region_name=region,
|
|
55
|
+
endpoint_url=endpoint_url,
|
|
56
|
+
)
|
|
57
|
+
self.table_name = table_name
|
|
58
|
+
|
|
59
|
+
if auto_create_table:
|
|
60
|
+
self._create_table()
|
|
61
|
+
|
|
62
|
+
self._table = self._dynamodb.Table(table_name)
|
|
63
|
+
logger.info(f"Connected to DynamoDB table: {table_name}")
|
|
64
|
+
|
|
65
|
+
def _create_table(self) -> None:
|
|
66
|
+
"""Create table if not exists."""
|
|
67
|
+
try:
|
|
68
|
+
self._client.describe_table(TableName=self.table_name)
|
|
69
|
+
except self._client.exceptions.ResourceNotFoundException:
|
|
70
|
+
self._client.create_table(
|
|
71
|
+
TableName=self.table_name,
|
|
72
|
+
KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
|
|
73
|
+
AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}],
|
|
74
|
+
BillingMode="PAY_PER_REQUEST",
|
|
75
|
+
)
|
|
76
|
+
waiter = self._client.get_waiter("table_exists")
|
|
77
|
+
waiter.wait(TableName=self.table_name)
|
|
78
|
+
|
|
79
|
+
# Enable TTL
|
|
80
|
+
self._client.update_time_to_live(
|
|
81
|
+
TableName=self.table_name,
|
|
82
|
+
TimeToLiveSpecification={"Enabled": True, "AttributeName": "ttl"}
|
|
83
|
+
)
|
|
84
|
+
logger.info(f"Created DynamoDB table: {self.table_name}")
|
|
85
|
+
|
|
86
|
+
def get(self, key: str) -> Optional[Any]:
|
|
87
|
+
"""Get a value by key."""
|
|
88
|
+
response = self._table.get_item(Key={"pk": key})
|
|
89
|
+
item = response.get("Item")
|
|
90
|
+
|
|
91
|
+
if not item:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Check TTL
|
|
95
|
+
if item.get("ttl") and item["ttl"] <= int(time.time()):
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
value = item.get("value")
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
try:
|
|
101
|
+
return json.loads(value)
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
return value
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
def set(
|
|
107
|
+
self,
|
|
108
|
+
key: str,
|
|
109
|
+
value: Any,
|
|
110
|
+
ttl: Optional[int] = None
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Set a value with optional TTL."""
|
|
113
|
+
item = {
|
|
114
|
+
"pk": key,
|
|
115
|
+
"value": json.dumps(value) if not isinstance(value, str) else value,
|
|
116
|
+
"updated_at": int(time.time()),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ttl:
|
|
120
|
+
item["ttl"] = int(time.time()) + ttl
|
|
121
|
+
|
|
122
|
+
self._table.put_item(Item=item)
|
|
123
|
+
|
|
124
|
+
def delete(self, key: str) -> bool:
|
|
125
|
+
"""Delete a key."""
|
|
126
|
+
response = self._table.delete_item(
|
|
127
|
+
Key={"pk": key},
|
|
128
|
+
ReturnValues="ALL_OLD"
|
|
129
|
+
)
|
|
130
|
+
return "Attributes" in response
|
|
131
|
+
|
|
132
|
+
def exists(self, key: str) -> bool:
|
|
133
|
+
"""Check if a key exists."""
|
|
134
|
+
response = self._table.get_item(
|
|
135
|
+
Key={"pk": key},
|
|
136
|
+
ProjectionExpression="pk,#t",
|
|
137
|
+
ExpressionAttributeNames={"#t": "ttl"}
|
|
138
|
+
)
|
|
139
|
+
item = response.get("Item")
|
|
140
|
+
if not item:
|
|
141
|
+
return False
|
|
142
|
+
if item.get("ttl") and item["ttl"] <= int(time.time()):
|
|
143
|
+
return False
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def keys(self, pattern: str = "*") -> List[str]:
|
|
147
|
+
"""List keys matching pattern."""
|
|
148
|
+
# DynamoDB scan is expensive, use with caution
|
|
149
|
+
response = self._table.scan(ProjectionExpression="pk")
|
|
150
|
+
keys = [item["pk"] for item in response.get("Items", [])]
|
|
151
|
+
|
|
152
|
+
if pattern != "*":
|
|
153
|
+
import fnmatch
|
|
154
|
+
keys = [k for k in keys if fnmatch.fnmatch(k, pattern)]
|
|
155
|
+
|
|
156
|
+
return keys
|
|
157
|
+
|
|
158
|
+
def ttl(self, key: str) -> Optional[int]:
|
|
159
|
+
"""Get remaining TTL in seconds."""
|
|
160
|
+
response = self._table.get_item(
|
|
161
|
+
Key={"pk": key},
|
|
162
|
+
ProjectionExpression="#t",
|
|
163
|
+
ExpressionAttributeNames={"#t": "ttl"}
|
|
164
|
+
)
|
|
165
|
+
item = response.get("Item")
|
|
166
|
+
if not item or "ttl" not in item:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
remaining = item["ttl"] - int(time.time())
|
|
170
|
+
if remaining <= 0:
|
|
171
|
+
return None
|
|
172
|
+
return remaining
|
|
173
|
+
|
|
174
|
+
def expire(self, key: str, ttl: int) -> bool:
|
|
175
|
+
"""Set TTL on existing key."""
|
|
176
|
+
try:
|
|
177
|
+
self._table.update_item(
|
|
178
|
+
Key={"pk": key},
|
|
179
|
+
UpdateExpression="SET #t = :ttl",
|
|
180
|
+
ExpressionAttributeNames={"#t": "ttl"},
|
|
181
|
+
ExpressionAttributeValues={":ttl": int(time.time()) + ttl},
|
|
182
|
+
ConditionExpression="attribute_exists(pk)"
|
|
183
|
+
)
|
|
184
|
+
return True
|
|
185
|
+
except self._client.exceptions.ConditionalCheckFailedException:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def hget(self, key: str, field: str) -> Optional[Any]:
|
|
189
|
+
"""Get a field from a hash."""
|
|
190
|
+
value = self.get(key)
|
|
191
|
+
if not isinstance(value, dict):
|
|
192
|
+
return None
|
|
193
|
+
return value.get(field)
|
|
194
|
+
|
|
195
|
+
def hset(self, key: str, field: str, value: Any) -> None:
|
|
196
|
+
"""Set a field in a hash."""
|
|
197
|
+
current = self.get(key)
|
|
198
|
+
if not isinstance(current, dict):
|
|
199
|
+
current = {}
|
|
200
|
+
current[field] = value
|
|
201
|
+
self.set(key, current)
|
|
202
|
+
|
|
203
|
+
def hgetall(self, key: str) -> Dict[str, Any]:
|
|
204
|
+
"""Get all fields from a hash."""
|
|
205
|
+
value = self.get(key)
|
|
206
|
+
if not isinstance(value, dict):
|
|
207
|
+
return {}
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
def hdel(self, key: str, *fields: str) -> int:
|
|
211
|
+
"""Delete fields from a hash."""
|
|
212
|
+
current = self.get(key)
|
|
213
|
+
if not isinstance(current, dict):
|
|
214
|
+
return 0
|
|
215
|
+
count = 0
|
|
216
|
+
for field in fields:
|
|
217
|
+
if field in current:
|
|
218
|
+
del current[field]
|
|
219
|
+
count += 1
|
|
220
|
+
if count > 0:
|
|
221
|
+
self.set(key, current)
|
|
222
|
+
return count
|
|
223
|
+
|
|
224
|
+
def close(self) -> None:
|
|
225
|
+
"""Close the store."""
|
|
226
|
+
pass # boto3 handles cleanup
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firestore implementation of StateStore.
|
|
3
|
+
|
|
4
|
+
Requires: google-cloud-firestore
|
|
5
|
+
Install: pip install google-cloud-firestore
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .base import StateStore
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FirestoreStateStore(StateStore):
|
|
20
|
+
"""
|
|
21
|
+
Firestore-based state store.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
store = FirestoreStateStore(
|
|
25
|
+
project="my-project",
|
|
26
|
+
collection="praisonai_state"
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
project: Optional[str] = None,
|
|
33
|
+
collection: str = "praisonai_state",
|
|
34
|
+
credentials_path: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
try:
|
|
37
|
+
from google.cloud import firestore
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise ImportError(
|
|
40
|
+
"google-cloud-firestore is required for Firestore support. "
|
|
41
|
+
"Install with: pip install google-cloud-firestore"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if credentials_path:
|
|
45
|
+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
|
|
46
|
+
|
|
47
|
+
project = project or os.getenv("FIRESTORE_PROJECT")
|
|
48
|
+
self._client = firestore.Client(project=project)
|
|
49
|
+
self._collection = self._client.collection(collection)
|
|
50
|
+
|
|
51
|
+
logger.info(f"Connected to Firestore collection: {collection}")
|
|
52
|
+
|
|
53
|
+
def get(self, key: str) -> Optional[Any]:
|
|
54
|
+
"""Get a value by key."""
|
|
55
|
+
doc = self._collection.document(key).get()
|
|
56
|
+
if not doc.exists:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
data = doc.to_dict()
|
|
60
|
+
|
|
61
|
+
# Check TTL
|
|
62
|
+
if data.get("expires_at") and data["expires_at"] <= time.time():
|
|
63
|
+
self._collection.document(key).delete()
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return data.get("value")
|
|
67
|
+
|
|
68
|
+
def set(
|
|
69
|
+
self,
|
|
70
|
+
key: str,
|
|
71
|
+
value: Any,
|
|
72
|
+
ttl: Optional[int] = None
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Set a value with optional TTL."""
|
|
75
|
+
data = {"value": value, "updated_at": time.time()}
|
|
76
|
+
|
|
77
|
+
if ttl:
|
|
78
|
+
data["expires_at"] = time.time() + ttl
|
|
79
|
+
|
|
80
|
+
self._collection.document(key).set(data)
|
|
81
|
+
|
|
82
|
+
def delete(self, key: str) -> bool:
|
|
83
|
+
"""Delete a key."""
|
|
84
|
+
doc = self._collection.document(key)
|
|
85
|
+
if doc.get().exists:
|
|
86
|
+
doc.delete()
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def exists(self, key: str) -> bool:
|
|
91
|
+
"""Check if a key exists."""
|
|
92
|
+
doc = self._collection.document(key).get()
|
|
93
|
+
if not doc.exists:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
data = doc.to_dict()
|
|
97
|
+
if data.get("expires_at") and data["expires_at"] <= time.time():
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def keys(self, pattern: str = "*") -> List[str]:
|
|
103
|
+
"""List keys matching pattern."""
|
|
104
|
+
docs = self._collection.stream()
|
|
105
|
+
keys = [doc.id for doc in docs]
|
|
106
|
+
|
|
107
|
+
if pattern != "*":
|
|
108
|
+
import fnmatch
|
|
109
|
+
keys = [k for k in keys if fnmatch.fnmatch(k, pattern)]
|
|
110
|
+
|
|
111
|
+
return keys
|
|
112
|
+
|
|
113
|
+
def ttl(self, key: str) -> Optional[int]:
|
|
114
|
+
"""Get remaining TTL in seconds."""
|
|
115
|
+
doc = self._collection.document(key).get()
|
|
116
|
+
if not doc.exists:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
data = doc.to_dict()
|
|
120
|
+
if "expires_at" not in data:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
remaining = data["expires_at"] - time.time()
|
|
124
|
+
if remaining <= 0:
|
|
125
|
+
return None
|
|
126
|
+
return int(remaining)
|
|
127
|
+
|
|
128
|
+
def expire(self, key: str, ttl: int) -> bool:
|
|
129
|
+
"""Set TTL on existing key."""
|
|
130
|
+
doc = self._collection.document(key)
|
|
131
|
+
if not doc.get().exists:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
doc.update({"expires_at": time.time() + ttl})
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def hget(self, key: str, field: str) -> Optional[Any]:
|
|
138
|
+
"""Get a field from a hash."""
|
|
139
|
+
value = self.get(key)
|
|
140
|
+
if not isinstance(value, dict):
|
|
141
|
+
return None
|
|
142
|
+
return value.get(field)
|
|
143
|
+
|
|
144
|
+
def hset(self, key: str, field: str, value: Any) -> None:
|
|
145
|
+
"""Set a field in a hash."""
|
|
146
|
+
current = self.get(key)
|
|
147
|
+
if not isinstance(current, dict):
|
|
148
|
+
current = {}
|
|
149
|
+
current[field] = value
|
|
150
|
+
self.set(key, current)
|
|
151
|
+
|
|
152
|
+
def hgetall(self, key: str) -> Dict[str, Any]:
|
|
153
|
+
"""Get all fields from a hash."""
|
|
154
|
+
value = self.get(key)
|
|
155
|
+
if not isinstance(value, dict):
|
|
156
|
+
return {}
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
def hdel(self, key: str, *fields: str) -> int:
|
|
160
|
+
"""Delete fields from a hash."""
|
|
161
|
+
current = self.get(key)
|
|
162
|
+
if not isinstance(current, dict):
|
|
163
|
+
return 0
|
|
164
|
+
count = 0
|
|
165
|
+
for field in fields:
|
|
166
|
+
if field in current:
|
|
167
|
+
del current[field]
|
|
168
|
+
count += 1
|
|
169
|
+
if count > 0:
|
|
170
|
+
self.set(key, current)
|
|
171
|
+
return count
|
|
172
|
+
|
|
173
|
+
def close(self) -> None:
|
|
174
|
+
"""Close the store."""
|
|
175
|
+
pass # Firestore client handles cleanup
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Cloud Storage implementation of StateStore.
|
|
3
|
+
|
|
4
|
+
Requires: google-cloud-storage
|
|
5
|
+
Install: pip install google-cloud-storage
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from .base import StateStore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GCSStateStore(StateStore):
|
|
19
|
+
"""
|
|
20
|
+
Google Cloud Storage-based state store.
|
|
21
|
+
|
|
22
|
+
Stores state as JSON objects in GCS buckets.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
store = GCSStateStore(
|
|
26
|
+
bucket_name="my-praisonai-state",
|
|
27
|
+
prefix="state/"
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
bucket_name: str,
|
|
34
|
+
prefix: str = "praisonai_state/",
|
|
35
|
+
project: Optional[str] = None,
|
|
36
|
+
credentials_path: Optional[str] = None,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize GCS state store.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
bucket_name: GCS bucket name
|
|
43
|
+
prefix: Object prefix for state keys
|
|
44
|
+
project: GCP project ID (optional)
|
|
45
|
+
credentials_path: Path to service account JSON (optional)
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
from google.cloud import storage
|
|
49
|
+
if credentials_path:
|
|
50
|
+
self._client = storage.Client.from_service_account_json(
|
|
51
|
+
credentials_path, project=project
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
self._client = storage.Client(project=project)
|
|
55
|
+
except ImportError:
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"google-cloud-storage is required for GCS support. "
|
|
58
|
+
"Install with: pip install google-cloud-storage"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.bucket_name = bucket_name
|
|
62
|
+
self.prefix = prefix
|
|
63
|
+
self._bucket = self._client.bucket(bucket_name)
|
|
64
|
+
|
|
65
|
+
def _key_to_path(self, key: str) -> str:
|
|
66
|
+
"""Convert key to GCS object path."""
|
|
67
|
+
return f"{self.prefix}{key}.json"
|
|
68
|
+
|
|
69
|
+
def get(self, key: str) -> Optional[Dict[str, Any]]:
|
|
70
|
+
"""Get state by key."""
|
|
71
|
+
try:
|
|
72
|
+
blob = self._bucket.blob(self._key_to_path(key))
|
|
73
|
+
if blob.exists():
|
|
74
|
+
data = json.loads(blob.download_as_text())
|
|
75
|
+
# Check TTL
|
|
76
|
+
if "_ttl_expires" in data:
|
|
77
|
+
if time.time() > data["_ttl_expires"]:
|
|
78
|
+
self.delete(key)
|
|
79
|
+
return None
|
|
80
|
+
del data["_ttl_expires"]
|
|
81
|
+
return data
|
|
82
|
+
return None
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Error getting state {key}: {e}")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def set(self, key: str, value: Dict[str, Any], ttl: Optional[int] = None) -> bool:
|
|
88
|
+
"""Set state by key with optional TTL in seconds."""
|
|
89
|
+
try:
|
|
90
|
+
data = value.copy()
|
|
91
|
+
if ttl:
|
|
92
|
+
data["_ttl_expires"] = time.time() + ttl
|
|
93
|
+
|
|
94
|
+
blob = self._bucket.blob(self._key_to_path(key))
|
|
95
|
+
blob.upload_from_string(
|
|
96
|
+
json.dumps(data, default=str),
|
|
97
|
+
content_type="application/json"
|
|
98
|
+
)
|
|
99
|
+
return True
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error setting state {key}: {e}")
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def delete(self, key: str) -> bool:
|
|
105
|
+
"""Delete state by key."""
|
|
106
|
+
try:
|
|
107
|
+
blob = self._bucket.blob(self._key_to_path(key))
|
|
108
|
+
if blob.exists():
|
|
109
|
+
blob.delete()
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error deleting state {key}: {e}")
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def exists(self, key: str) -> bool:
|
|
117
|
+
"""Check if key exists."""
|
|
118
|
+
try:
|
|
119
|
+
blob = self._bucket.blob(self._key_to_path(key))
|
|
120
|
+
return blob.exists()
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def list_keys(self, prefix: Optional[str] = None) -> List[str]:
|
|
125
|
+
"""List all keys with optional prefix filter."""
|
|
126
|
+
try:
|
|
127
|
+
search_prefix = self.prefix
|
|
128
|
+
if prefix:
|
|
129
|
+
search_prefix = f"{self.prefix}{prefix}"
|
|
130
|
+
|
|
131
|
+
blobs = self._client.list_blobs(self.bucket_name, prefix=search_prefix)
|
|
132
|
+
keys = []
|
|
133
|
+
for blob in blobs:
|
|
134
|
+
# Remove prefix and .json suffix
|
|
135
|
+
key = blob.name[len(self.prefix):]
|
|
136
|
+
if key.endswith(".json"):
|
|
137
|
+
key = key[:-5]
|
|
138
|
+
keys.append(key)
|
|
139
|
+
return keys
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Error listing keys: {e}")
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
def clear(self, prefix: Optional[str] = None) -> int:
|
|
145
|
+
"""Clear all keys with optional prefix filter."""
|
|
146
|
+
keys = self.list_keys(prefix)
|
|
147
|
+
count = 0
|
|
148
|
+
for key in keys:
|
|
149
|
+
if self.delete(key):
|
|
150
|
+
count += 1
|
|
151
|
+
return count
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
"""Close the store."""
|
|
155
|
+
self._client.close()
|