vystak 0.1.0__tar.gz

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 (65) hide show
  1. vystak-0.1.0/.gitignore +40 -0
  2. vystak-0.1.0/PKG-INFO +63 -0
  3. vystak-0.1.0/README.md +35 -0
  4. vystak-0.1.0/pyproject.toml +50 -0
  5. vystak-0.1.0/src/vystak/__init__.py +126 -0
  6. vystak-0.1.0/src/vystak/hash/__init__.py +6 -0
  7. vystak-0.1.0/src/vystak/hash/hasher.py +18 -0
  8. vystak-0.1.0/src/vystak/hash/tree.py +82 -0
  9. vystak-0.1.0/src/vystak/ir/__init__.py +1 -0
  10. vystak-0.1.0/src/vystak/providers/__init__.py +23 -0
  11. vystak-0.1.0/src/vystak/providers/base.py +77 -0
  12. vystak-0.1.0/src/vystak/provisioning/__init__.py +37 -0
  13. vystak-0.1.0/src/vystak/provisioning/graph.py +122 -0
  14. vystak-0.1.0/src/vystak/provisioning/grouping.py +26 -0
  15. vystak-0.1.0/src/vystak/provisioning/health.py +62 -0
  16. vystak-0.1.0/src/vystak/provisioning/listener.py +68 -0
  17. vystak-0.1.0/src/vystak/provisioning/node.py +53 -0
  18. vystak-0.1.0/src/vystak/schema/__init__.py +93 -0
  19. vystak-0.1.0/src/vystak/schema/agent.py +49 -0
  20. vystak-0.1.0/src/vystak/schema/channel.py +23 -0
  21. vystak-0.1.0/src/vystak/schema/common.py +32 -0
  22. vystak-0.1.0/src/vystak/schema/config_loader.py +43 -0
  23. vystak-0.1.0/src/vystak/schema/gateway.py +19 -0
  24. vystak-0.1.0/src/vystak/schema/loader.py +42 -0
  25. vystak-0.1.0/src/vystak/schema/mcp.py +15 -0
  26. vystak-0.1.0/src/vystak/schema/model.py +20 -0
  27. vystak-0.1.0/src/vystak/schema/multi_loader.py +69 -0
  28. vystak-0.1.0/src/vystak/schema/openai.py +136 -0
  29. vystak-0.1.0/src/vystak/schema/platform.py +12 -0
  30. vystak-0.1.0/src/vystak/schema/provider.py +10 -0
  31. vystak-0.1.0/src/vystak/schema/resource.py +36 -0
  32. vystak-0.1.0/src/vystak/schema/secret.py +16 -0
  33. vystak-0.1.0/src/vystak/schema/service.py +65 -0
  34. vystak-0.1.0/src/vystak/schema/skill.py +24 -0
  35. vystak-0.1.0/src/vystak/schema/workspace.py +20 -0
  36. vystak-0.1.0/src/vystak/stores/__init__.py +5 -0
  37. vystak-0.1.0/src/vystak/stores/sqlite.py +117 -0
  38. vystak-0.1.0/tests/test_agent.py +178 -0
  39. vystak-0.1.0/tests/test_base.py +107 -0
  40. vystak-0.1.0/tests/test_channel.py +30 -0
  41. vystak-0.1.0/tests/test_common.py +51 -0
  42. vystak-0.1.0/tests/test_config_loader.py +91 -0
  43. vystak-0.1.0/tests/test_gateway.py +61 -0
  44. vystak-0.1.0/tests/test_graph.py +145 -0
  45. vystak-0.1.0/tests/test_grouping.py +97 -0
  46. vystak-0.1.0/tests/test_hasher.py +47 -0
  47. vystak-0.1.0/tests/test_health.py +76 -0
  48. vystak-0.1.0/tests/test_integration.py +110 -0
  49. vystak-0.1.0/tests/test_loader.py +214 -0
  50. vystak-0.1.0/tests/test_mcp.py +52 -0
  51. vystak-0.1.0/tests/test_model.py +64 -0
  52. vystak-0.1.0/tests/test_multi_loader.py +142 -0
  53. vystak-0.1.0/tests/test_openai_schema.py +160 -0
  54. vystak-0.1.0/tests/test_platform.py +31 -0
  55. vystak-0.1.0/tests/test_provider.py +33 -0
  56. vystak-0.1.0/tests/test_resource.py +68 -0
  57. vystak-0.1.0/tests/test_schema_exports.py +74 -0
  58. vystak-0.1.0/tests/test_secret.py +31 -0
  59. vystak-0.1.0/tests/test_service.py +109 -0
  60. vystak-0.1.0/tests/test_skill.py +51 -0
  61. vystak-0.1.0/tests/test_slack_channel.py +52 -0
  62. vystak-0.1.0/tests/test_sqlite_store.py +111 -0
  63. vystak-0.1.0/tests/test_tree.py +130 -0
  64. vystak-0.1.0/tests/test_version.py +5 -0
  65. vystak-0.1.0/tests/test_workspace.py +51 -0
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.egg
10
+ .venv/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .pyright/
14
+
15
+ # TypeScript
16
+ node_modules/
17
+ *.tsbuildinfo
18
+ coverage/
19
+
20
+ # IDE
21
+ .idea/
22
+ .vscode/
23
+ *.swp
24
+ *.swo
25
+ *~
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Environment
32
+ .env
33
+ .env.local
34
+
35
+ # Vystak generated output (was .agentstack/ before rename)
36
+ .vystak/
37
+ .agentstack/
38
+
39
+ # Tooling workspaces (brainstorm sessions, local agent state)
40
+ .superpowers/
vystak-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: vystak
3
+ Version: 0.1.0
4
+ Summary: Vystak core SDK — declarative AI agent orchestration
5
+ Project-URL: Homepage, https://vystak.dev
6
+ Project-URL: Documentation, https://vystak.dev/docs
7
+ Project-URL: Repository, https://github.com/vystak/vystak
8
+ Project-URL: Issues, https://github.com/vystak/vystak/issues
9
+ Author-email: Anatoliy Kolodkin <11351966+akolodkin@users.noreply.github.com>
10
+ License-Expression: Apache-2.0
11
+ Keywords: agent,ai,declarative,infrastructure,langchain,langgraph,orchestration
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: pydantic>=2.0
26
+ Requires-Dist: pyyaml>=6.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # vystak
30
+
31
+ Core SDK for Vystak — declarative, platform-agnostic orchestration for AI agents.
32
+
33
+ This package provides the foundational schema, hashing engine, provisioning graph, and provider base classes that the rest of the Vystak ecosystem builds on. You typically don't install this directly — use [`vystak-cli`](https://pypi.org/project/vystak-cli/) instead, which pulls it in.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install vystak
39
+ ```
40
+
41
+ ## What's in this package
42
+
43
+ - **`vystak.schema`** — Pydantic models for `Agent`, `Model`, `Provider`, `Platform`, `Channel`, `Service`, `Skill`, `Workspace`, `Secret`, `Mcp`. This is the contract adapters and providers consume.
44
+ - **`vystak.hash`** — content-addressable hashing (`AgentHashTree`) for stateless change detection.
45
+ - **`vystak.provisioning`** — `ProvisionGraph`, a DAG of `Provisionable` nodes for topological resource rollout with health checks.
46
+ - **`vystak.providers`** — `PlatformProvider`, `FrameworkAdapter`, `ChannelAdapter` ABCs.
47
+ - **`vystak.stores`** — async SQLite-backed key-value store for long-term memory.
48
+
49
+ ## Example
50
+
51
+ ```python
52
+ import vystak as ast
53
+
54
+ anthropic = ast.Provider(name="anthropic", type="anthropic")
55
+ model = ast.Model(name="claude", provider=anthropic, model_name="claude-sonnet-4-20250514")
56
+ agent = ast.Agent(name="support-bot", model=model)
57
+ ```
58
+
59
+ See the [main repository](https://github.com/vystak/vystak) for the full CLI, adapters, and providers.
60
+
61
+ ## License
62
+
63
+ Apache-2.0
vystak-0.1.0/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # vystak
2
+
3
+ Core SDK for Vystak — declarative, platform-agnostic orchestration for AI agents.
4
+
5
+ This package provides the foundational schema, hashing engine, provisioning graph, and provider base classes that the rest of the Vystak ecosystem builds on. You typically don't install this directly — use [`vystak-cli`](https://pypi.org/project/vystak-cli/) instead, which pulls it in.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install vystak
11
+ ```
12
+
13
+ ## What's in this package
14
+
15
+ - **`vystak.schema`** — Pydantic models for `Agent`, `Model`, `Provider`, `Platform`, `Channel`, `Service`, `Skill`, `Workspace`, `Secret`, `Mcp`. This is the contract adapters and providers consume.
16
+ - **`vystak.hash`** — content-addressable hashing (`AgentHashTree`) for stateless change detection.
17
+ - **`vystak.provisioning`** — `ProvisionGraph`, a DAG of `Provisionable` nodes for topological resource rollout with health checks.
18
+ - **`vystak.providers`** — `PlatformProvider`, `FrameworkAdapter`, `ChannelAdapter` ABCs.
19
+ - **`vystak.stores`** — async SQLite-backed key-value store for long-term memory.
20
+
21
+ ## Example
22
+
23
+ ```python
24
+ import vystak as ast
25
+
26
+ anthropic = ast.Provider(name="anthropic", type="anthropic")
27
+ model = ast.Model(name="claude", provider=anthropic, model_name="claude-sonnet-4-20250514")
28
+ agent = ast.Agent(name="support-bot", model=model)
29
+ ```
30
+
31
+ See the [main repository](https://github.com/vystak/vystak) for the full CLI, adapters, and providers.
32
+
33
+ ## License
34
+
35
+ Apache-2.0
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "vystak"
3
+ version = "0.1.0"
4
+ description = "Vystak core SDK — declarative AI agent orchestration"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ authors = [
9
+ { name = "Anatoliy Kolodkin", email = "11351966+akolodkin@users.noreply.github.com" },
10
+ ]
11
+ keywords = [
12
+ "ai",
13
+ "agent",
14
+ "orchestration",
15
+ "langchain",
16
+ "langgraph",
17
+ "infrastructure",
18
+ "declarative",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: Apache Software License",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Topic :: System :: Systems Administration",
31
+ "Typing :: Typed",
32
+ ]
33
+ dependencies = [
34
+ "pydantic>=2.0",
35
+ "pyyaml>=6.0",
36
+ "aiosqlite>=0.20",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://vystak.dev"
41
+ Documentation = "https://vystak.dev/docs"
42
+ Repository = "https://github.com/vystak/vystak"
43
+ Issues = "https://github.com/vystak/vystak/issues"
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/vystak"]
@@ -0,0 +1,126 @@
1
+ """Vystak — declarative AI agent orchestration."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Schema models
6
+ # Hash engine
7
+ from vystak.hash import AgentHashTree, hash_agent, hash_dict, hash_model
8
+
9
+ # Provider ABCs and supporting types
10
+ from vystak.providers import (
11
+ AgentStatus,
12
+ ChannelAdapter,
13
+ DeployPlan,
14
+ DeployResult,
15
+ FrameworkAdapter,
16
+ GeneratedCode,
17
+ PlatformProvider,
18
+ ValidationError,
19
+ )
20
+
21
+ # Provisioning engine
22
+ from vystak.provisioning import (
23
+ CommandHealthCheck,
24
+ CycleError,
25
+ HealthCheck,
26
+ HttpHealthCheck,
27
+ NoopHealthCheck,
28
+ Provisionable,
29
+ ProvisionError,
30
+ ProvisionGraph,
31
+ ProvisionResult,
32
+ TcpHealthCheck,
33
+ )
34
+ from vystak.schema import (
35
+ Agent,
36
+ Cache,
37
+ Channel,
38
+ ChannelProvider,
39
+ ChannelType,
40
+ Database,
41
+ Embedding,
42
+ Gateway,
43
+ McpServer,
44
+ McpTransport,
45
+ Model,
46
+ NamedModel,
47
+ ObjectStore,
48
+ Platform,
49
+ Postgres,
50
+ Provider,
51
+ Qdrant,
52
+ Queue,
53
+ Redis,
54
+ Resource,
55
+ Secret,
56
+ Service,
57
+ SessionStore,
58
+ Skill,
59
+ SkillRequirements,
60
+ SlackChannel,
61
+ Sqlite,
62
+ VectorStore,
63
+ Workspace,
64
+ WorkspaceType,
65
+ )
66
+
67
+ # Loader
68
+ from vystak.schema.loader import dump_agent, load_agent
69
+
70
+ __all__ = [
71
+ "__version__",
72
+ "Agent",
73
+ "Cache",
74
+ "Channel",
75
+ "ChannelProvider",
76
+ "ChannelType",
77
+ "Database",
78
+ "Embedding",
79
+ "Gateway",
80
+ "McpServer",
81
+ "McpTransport",
82
+ "Model",
83
+ "NamedModel",
84
+ "ObjectStore",
85
+ "Platform",
86
+ "Postgres",
87
+ "Provider",
88
+ "Qdrant",
89
+ "Queue",
90
+ "Redis",
91
+ "Resource",
92
+ "Secret",
93
+ "Service",
94
+ "SessionStore",
95
+ "Skill",
96
+ "SkillRequirements",
97
+ "SlackChannel",
98
+ "Sqlite",
99
+ "VectorStore",
100
+ "Workspace",
101
+ "WorkspaceType",
102
+ "AgentHashTree",
103
+ "hash_agent",
104
+ "hash_dict",
105
+ "hash_model",
106
+ "dump_agent",
107
+ "load_agent",
108
+ "AgentStatus",
109
+ "ChannelAdapter",
110
+ "DeployPlan",
111
+ "DeployResult",
112
+ "FrameworkAdapter",
113
+ "GeneratedCode",
114
+ "PlatformProvider",
115
+ "ValidationError",
116
+ "CommandHealthCheck",
117
+ "CycleError",
118
+ "HealthCheck",
119
+ "HttpHealthCheck",
120
+ "NoopHealthCheck",
121
+ "Provisionable",
122
+ "ProvisionError",
123
+ "ProvisionGraph",
124
+ "ProvisionResult",
125
+ "TcpHealthCheck",
126
+ ]
@@ -0,0 +1,6 @@
1
+ """Content-addressable hash engine for stateless change detection."""
2
+
3
+ from vystak.hash.hasher import hash_dict, hash_model
4
+ from vystak.hash.tree import AgentHashTree, hash_agent
5
+
6
+ __all__ = ["AgentHashTree", "hash_agent", "hash_dict", "hash_model"]
@@ -0,0 +1,18 @@
1
+ """Leaf-level hashing for Pydantic models and dicts."""
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ def hash_model(model: BaseModel) -> str:
10
+ """SHA-256 of canonical JSON representation of a Pydantic model."""
11
+ canonical = json.dumps(model.model_dump(mode="python"), sort_keys=True, default=str)
12
+ return hashlib.sha256(canonical.encode()).hexdigest()
13
+
14
+
15
+ def hash_dict(data: dict) -> str:
16
+ """SHA-256 of canonical JSON representation of a dict."""
17
+ canonical = json.dumps(data, sort_keys=True, default=str)
18
+ return hashlib.sha256(canonical.encode()).hexdigest()
@@ -0,0 +1,82 @@
1
+ """Hash tree composition for agent definitions."""
2
+
3
+ import hashlib
4
+ from dataclasses import dataclass
5
+
6
+ from vystak.hash.hasher import hash_model
7
+ from vystak.schema.agent import Agent
8
+
9
+
10
+ @dataclass
11
+ class AgentHashTree:
12
+ """Per-section hashes for an agent, enabling partial deploy detection."""
13
+
14
+ brain: str
15
+ skills: str
16
+ mcp_servers: str
17
+ channels: str
18
+ workspace: str
19
+ resources: str
20
+ secrets: str
21
+ sessions: str
22
+ memory: str
23
+ services: str
24
+ root: str
25
+
26
+
27
+ def _hash_list(items: list) -> str:
28
+ if not items:
29
+ return hashlib.sha256(b"[]").hexdigest()
30
+ individual = sorted(hash_model(item) for item in items)
31
+ combined = "|".join(individual)
32
+ return hashlib.sha256(combined.encode()).hexdigest()
33
+
34
+
35
+ def _hash_optional(item) -> str:
36
+ if item is None:
37
+ return hashlib.sha256(b"null").hexdigest()
38
+ return hash_model(item)
39
+
40
+
41
+ def hash_agent(agent: Agent) -> AgentHashTree:
42
+ """Compute the full hash tree for an agent definition."""
43
+ brain = hash_model(agent.model)
44
+ skills = _hash_list(agent.skills)
45
+ mcp_servers = _hash_list(agent.mcp_servers)
46
+ channels = _hash_list(agent.channels)
47
+ workspace = _hash_optional(agent.workspace)
48
+ resources = _hash_list(agent.resources)
49
+ secrets = _hash_list(agent.secrets)
50
+ sessions = _hash_optional(agent.sessions)
51
+ memory = _hash_optional(agent.memory)
52
+ services = _hash_list(agent.services)
53
+
54
+ sections = "|".join(
55
+ [
56
+ brain,
57
+ skills,
58
+ mcp_servers,
59
+ channels,
60
+ workspace,
61
+ resources,
62
+ secrets,
63
+ sessions,
64
+ memory,
65
+ services,
66
+ ]
67
+ )
68
+ root = hashlib.sha256(sections.encode()).hexdigest()
69
+
70
+ return AgentHashTree(
71
+ brain=brain,
72
+ skills=skills,
73
+ mcp_servers=mcp_servers,
74
+ channels=channels,
75
+ workspace=workspace,
76
+ resources=resources,
77
+ secrets=secrets,
78
+ sessions=sessions,
79
+ memory=memory,
80
+ services=services,
81
+ root=root,
82
+ )
@@ -0,0 +1 @@
1
+ """Intermediate representation consumed by framework adapters."""
@@ -0,0 +1,23 @@
1
+ """Provider base classes for platform and resource provisioning."""
2
+
3
+ from vystak.providers.base import (
4
+ AgentStatus,
5
+ ChannelAdapter,
6
+ DeployPlan,
7
+ DeployResult,
8
+ FrameworkAdapter,
9
+ GeneratedCode,
10
+ PlatformProvider,
11
+ ValidationError,
12
+ )
13
+
14
+ __all__ = [
15
+ "AgentStatus",
16
+ "ChannelAdapter",
17
+ "DeployPlan",
18
+ "DeployResult",
19
+ "FrameworkAdapter",
20
+ "GeneratedCode",
21
+ "PlatformProvider",
22
+ "ValidationError",
23
+ ]
@@ -0,0 +1,77 @@
1
+ """Abstract base classes for framework adapters, platform providers, and channel adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+
6
+ from vystak.schema.agent import Agent
7
+ from vystak.schema.channel import Channel
8
+
9
+
10
+ @dataclass
11
+ class GeneratedCode:
12
+ files: dict[str, str]
13
+ entrypoint: str
14
+
15
+
16
+ @dataclass
17
+ class DeployPlan:
18
+ agent_name: str
19
+ actions: list[str]
20
+ current_hash: str | None
21
+ target_hash: str
22
+ changes: dict[str, tuple[str | None, str]]
23
+
24
+
25
+ @dataclass
26
+ class DeployResult:
27
+ agent_name: str
28
+ success: bool
29
+ hash: str
30
+ message: str
31
+
32
+
33
+ @dataclass
34
+ class AgentStatus:
35
+ agent_name: str
36
+ running: bool
37
+ hash: str | None
38
+ info: dict = field(default_factory=dict)
39
+
40
+
41
+ @dataclass
42
+ class ValidationError:
43
+ field: str
44
+ message: str
45
+
46
+
47
+ class FrameworkAdapter(ABC):
48
+ @abstractmethod
49
+ def generate(self, agent: Agent) -> GeneratedCode: ...
50
+
51
+ @abstractmethod
52
+ def validate(self, agent: Agent) -> list[ValidationError]: ...
53
+
54
+
55
+ class PlatformProvider(ABC):
56
+ @abstractmethod
57
+ def plan(self, agent: Agent, current_hash: str | None) -> DeployPlan: ...
58
+
59
+ @abstractmethod
60
+ def apply(self, plan: DeployPlan) -> DeployResult: ...
61
+
62
+ @abstractmethod
63
+ def destroy(self, agent_name: str) -> None: ...
64
+
65
+ @abstractmethod
66
+ def status(self, agent_name: str) -> AgentStatus: ...
67
+
68
+ @abstractmethod
69
+ def get_hash(self, agent_name: str) -> str | None: ...
70
+
71
+
72
+ class ChannelAdapter(ABC):
73
+ @abstractmethod
74
+ def setup(self, agent: Agent, channel: Channel) -> None: ...
75
+
76
+ @abstractmethod
77
+ def teardown(self, channel: Channel) -> None: ...
@@ -0,0 +1,37 @@
1
+ """Provisioning engine — dependency graph, health checks, and provisionable protocol."""
2
+
3
+ from vystak.provisioning.graph import CycleError, ProvisionError, ProvisionGraph
4
+ from vystak.provisioning.grouping import group_agents_by_platform, platform_fingerprint
5
+ from vystak.provisioning.health import (
6
+ CommandHealthCheck,
7
+ HealthCheck,
8
+ HttpHealthCheck,
9
+ NoopHealthCheck,
10
+ TcpHealthCheck,
11
+ )
12
+ from vystak.provisioning.listener import (
13
+ NullListener,
14
+ PrintListener,
15
+ ProvisionEvent,
16
+ ProvisionListener,
17
+ )
18
+ from vystak.provisioning.node import Provisionable, ProvisionResult
19
+
20
+ __all__ = [
21
+ "CommandHealthCheck",
22
+ "CycleError",
23
+ "group_agents_by_platform",
24
+ "HealthCheck",
25
+ "HttpHealthCheck",
26
+ "NoopHealthCheck",
27
+ "NullListener",
28
+ "platform_fingerprint",
29
+ "PrintListener",
30
+ "Provisionable",
31
+ "ProvisionError",
32
+ "ProvisionEvent",
33
+ "ProvisionGraph",
34
+ "ProvisionListener",
35
+ "ProvisionResult",
36
+ "TcpHealthCheck",
37
+ ]
@@ -0,0 +1,122 @@
1
+ """Dependency graph for provisioning resources."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from vystak.provisioning.listener import NullListener, ProvisionEvent, ProvisionListener
6
+ from vystak.provisioning.node import Provisionable, ProvisionResult
7
+
8
+
9
+ class CycleError(Exception):
10
+ pass
11
+
12
+
13
+ class ProvisionError(Exception):
14
+ pass
15
+
16
+
17
+ @dataclass
18
+ class ProvisionGraph:
19
+ _nodes: dict[str, Provisionable] = field(default_factory=dict)
20
+ _implicit_deps: dict[str, list[str]] = field(default_factory=dict)
21
+ _listener: ProvisionListener = field(default_factory=NullListener)
22
+
23
+ def set_listener(self, listener: ProvisionListener) -> None:
24
+ """Set the event listener for provision progress."""
25
+ self._listener = listener
26
+
27
+ def add(self, node: Provisionable) -> None:
28
+ self._nodes[node.name] = node
29
+
30
+ def add_dependency(self, name: str, depends_on: str) -> None:
31
+ self._implicit_deps.setdefault(name, []).append(depends_on)
32
+
33
+ def _all_deps(self, name: str) -> list[str]:
34
+ node = self._nodes[name]
35
+ all_deps = list(node.depends_on)
36
+ all_deps.extend(self._implicit_deps.get(name, []))
37
+ return [d for d in all_deps if d in self._nodes]
38
+
39
+ def _resolve_order(self) -> list[str]:
40
+ reverse: dict[str, list[str]] = {name: [] for name in self._nodes}
41
+ in_degree: dict[str, int] = {name: 0 for name in self._nodes}
42
+
43
+ for name in self._nodes:
44
+ deps = self._all_deps(name)
45
+ in_degree[name] = len(deps)
46
+ for dep in deps:
47
+ reverse[dep].append(name)
48
+
49
+ queue = [n for n, d in in_degree.items() if d == 0]
50
+ order: list[str] = []
51
+
52
+ while queue:
53
+ node = queue.pop(0)
54
+ order.append(node)
55
+ for dependent in reverse[node]:
56
+ in_degree[dependent] -= 1
57
+ if in_degree[dependent] == 0:
58
+ queue.append(dependent)
59
+
60
+ if len(order) != len(self._nodes):
61
+ raise CycleError("Dependency cycle detected in provision graph")
62
+
63
+ return order
64
+
65
+ def execute(self) -> dict[str, ProvisionResult]:
66
+ order = self._resolve_order()
67
+ results: dict[str, ProvisionResult] = {}
68
+
69
+ for name in order:
70
+ node = self._nodes[name]
71
+
72
+ self._listener.on_start(
73
+ ProvisionEvent(
74
+ node_name=name,
75
+ event_type="start",
76
+ message=name,
77
+ )
78
+ )
79
+
80
+ # Pass listener to node if it supports it
81
+ if hasattr(node, "set_listener"):
82
+ node.set_listener(self._listener)
83
+
84
+ result = node.provision(context=results)
85
+ results[name] = result
86
+
87
+ if not result.success:
88
+ self._listener.on_error(
89
+ ProvisionEvent(
90
+ node_name=name,
91
+ event_type="error",
92
+ message=name,
93
+ detail=result.error or "",
94
+ )
95
+ )
96
+ raise ProvisionError(f"Failed to provision {name}: {result.error}")
97
+
98
+ self._listener.on_complete(
99
+ ProvisionEvent(
100
+ node_name=name,
101
+ event_type="complete",
102
+ message=name,
103
+ detail=result.info.get("detail", ""),
104
+ )
105
+ )
106
+
107
+ check = node.health_check()
108
+ self._listener.on_health_check(
109
+ ProvisionEvent(
110
+ node_name=name,
111
+ event_type="health_check",
112
+ message=f"Waiting for {name}",
113
+ )
114
+ )
115
+ check.wait()
116
+
117
+ return results
118
+
119
+ def destroy_all(self) -> None:
120
+ order = self._resolve_order()
121
+ for name in reversed(order):
122
+ self._nodes[name].destroy()