flock-core 0.5.11__py3-none-any.whl → 0.5.20__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (91) hide show
  1. flock/__init__.py +1 -1
  2. flock/agent/__init__.py +30 -0
  3. flock/agent/builder_helpers.py +192 -0
  4. flock/agent/builder_validator.py +169 -0
  5. flock/agent/component_lifecycle.py +325 -0
  6. flock/agent/context_resolver.py +141 -0
  7. flock/agent/mcp_integration.py +212 -0
  8. flock/agent/output_processor.py +304 -0
  9. flock/api/__init__.py +20 -0
  10. flock/{api_models.py → api/models.py} +0 -2
  11. flock/{service.py → api/service.py} +3 -3
  12. flock/cli.py +2 -2
  13. flock/components/__init__.py +41 -0
  14. flock/components/agent/__init__.py +22 -0
  15. flock/{components.py → components/agent/base.py} +4 -3
  16. flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
  17. flock/components/orchestrator/__init__.py +22 -0
  18. flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
  19. flock/components/orchestrator/circuit_breaker.py +95 -0
  20. flock/components/orchestrator/collection.py +143 -0
  21. flock/components/orchestrator/deduplication.py +78 -0
  22. flock/core/__init__.py +30 -0
  23. flock/core/agent.py +953 -0
  24. flock/{artifacts.py → core/artifacts.py} +1 -1
  25. flock/{context_provider.py → core/context_provider.py} +3 -3
  26. flock/core/orchestrator.py +1102 -0
  27. flock/{store.py → core/store.py} +99 -454
  28. flock/{subscription.py → core/subscription.py} +1 -1
  29. flock/dashboard/collector.py +5 -5
  30. flock/dashboard/graph_builder.py +7 -7
  31. flock/dashboard/routes/__init__.py +21 -0
  32. flock/dashboard/routes/control.py +327 -0
  33. flock/dashboard/routes/helpers.py +340 -0
  34. flock/dashboard/routes/themes.py +76 -0
  35. flock/dashboard/routes/traces.py +521 -0
  36. flock/dashboard/routes/websocket.py +108 -0
  37. flock/dashboard/service.py +43 -1316
  38. flock/engines/dspy/__init__.py +20 -0
  39. flock/engines/dspy/artifact_materializer.py +216 -0
  40. flock/engines/dspy/signature_builder.py +474 -0
  41. flock/engines/dspy/streaming_executor.py +858 -0
  42. flock/engines/dspy_engine.py +45 -1330
  43. flock/engines/examples/simple_batch_engine.py +2 -2
  44. flock/examples.py +7 -7
  45. flock/logging/logging.py +1 -16
  46. flock/models/__init__.py +10 -0
  47. flock/orchestrator/__init__.py +45 -0
  48. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  49. flock/orchestrator/artifact_manager.py +168 -0
  50. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  51. flock/orchestrator/component_runner.py +389 -0
  52. flock/orchestrator/context_builder.py +167 -0
  53. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  54. flock/orchestrator/event_emitter.py +167 -0
  55. flock/orchestrator/initialization.py +184 -0
  56. flock/orchestrator/lifecycle_manager.py +226 -0
  57. flock/orchestrator/mcp_manager.py +202 -0
  58. flock/orchestrator/scheduler.py +189 -0
  59. flock/orchestrator/server_manager.py +234 -0
  60. flock/orchestrator/tracing.py +147 -0
  61. flock/storage/__init__.py +10 -0
  62. flock/storage/artifact_aggregator.py +158 -0
  63. flock/storage/in_memory/__init__.py +6 -0
  64. flock/storage/in_memory/artifact_filter.py +114 -0
  65. flock/storage/in_memory/history_aggregator.py +115 -0
  66. flock/storage/sqlite/__init__.py +10 -0
  67. flock/storage/sqlite/agent_history_queries.py +154 -0
  68. flock/storage/sqlite/consumption_loader.py +100 -0
  69. flock/storage/sqlite/query_builder.py +112 -0
  70. flock/storage/sqlite/query_params_builder.py +91 -0
  71. flock/storage/sqlite/schema_manager.py +168 -0
  72. flock/storage/sqlite/summary_queries.py +194 -0
  73. flock/utils/__init__.py +14 -0
  74. flock/utils/async_utils.py +67 -0
  75. flock/{runtime.py → utils/runtime.py} +3 -3
  76. flock/utils/time_utils.py +53 -0
  77. flock/utils/type_resolution.py +38 -0
  78. flock/{utilities.py → utils/utilities.py} +2 -2
  79. flock/utils/validation.py +57 -0
  80. flock/utils/visibility.py +79 -0
  81. flock/utils/visibility_utils.py +134 -0
  82. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/METADATA +18 -4
  83. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -33
  84. flock/agent.py +0 -1578
  85. flock/orchestrator.py +0 -1983
  86. /flock/{visibility.py → core/visibility.py} +0 -0
  87. /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
  88. /flock/{helper → utils}/cli_helper.py +0 -0
  89. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
  90. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
  91. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,194 @@
1
+ """SQLite summary query utilities.
2
+
3
+ Provides focused methods for executing summary/aggregation queries.
4
+ Extracted from summarize_artifacts to reduce complexity and improve testability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ import aiosqlite
14
+
15
+
16
+ class SQLiteSummaryQueries:
17
+ """
18
+ Executes summary SQL queries for artifact statistics.
19
+
20
+ Each method handles one specific aggregation query, making them
21
+ simple, testable, and easy to maintain.
22
+ """
23
+
24
+ async def count_total(
25
+ self,
26
+ conn: aiosqlite.Connection,
27
+ where_clause: str,
28
+ params: tuple[Any, ...],
29
+ ) -> int:
30
+ """
31
+ Get total artifact count.
32
+
33
+ Args:
34
+ conn: Database connection
35
+ where_clause: SQL WHERE clause (e.g., " WHERE type = ?")
36
+ params: Parameter values for WHERE clause
37
+
38
+ Returns:
39
+ Total count of matching artifacts
40
+ """
41
+ count_query = f"SELECT COUNT(*) AS total FROM artifacts{where_clause}" # nosec B608
42
+ cursor = await conn.execute(count_query, params)
43
+ total_row = await cursor.fetchone()
44
+ await cursor.close()
45
+ return total_row["total"] if total_row else 0
46
+
47
+ async def group_by_type(
48
+ self,
49
+ conn: aiosqlite.Connection,
50
+ where_clause: str,
51
+ params: tuple[Any, ...],
52
+ ) -> dict[str, int]:
53
+ """
54
+ Get artifact counts grouped by type.
55
+
56
+ Args:
57
+ conn: Database connection
58
+ where_clause: SQL WHERE clause
59
+ params: Parameter values for WHERE clause
60
+
61
+ Returns:
62
+ Dict mapping canonical type names to counts
63
+ """
64
+ by_type_query = f"""
65
+ SELECT canonical_type, COUNT(*) AS count
66
+ FROM artifacts
67
+ {where_clause}
68
+ GROUP BY canonical_type
69
+ """ # nosec B608
70
+ cursor = await conn.execute(by_type_query, params)
71
+ by_type_rows = await cursor.fetchall()
72
+ await cursor.close()
73
+ return {row["canonical_type"]: row["count"] for row in by_type_rows}
74
+
75
+ async def group_by_producer(
76
+ self,
77
+ conn: aiosqlite.Connection,
78
+ where_clause: str,
79
+ params: tuple[Any, ...],
80
+ ) -> dict[str, int]:
81
+ """
82
+ Get artifact counts grouped by producer.
83
+
84
+ Args:
85
+ conn: Database connection
86
+ where_clause: SQL WHERE clause
87
+ params: Parameter values for WHERE clause
88
+
89
+ Returns:
90
+ Dict mapping producer names to counts
91
+ """
92
+ by_producer_query = f"""
93
+ SELECT produced_by, COUNT(*) AS count
94
+ FROM artifacts
95
+ {where_clause}
96
+ GROUP BY produced_by
97
+ """ # nosec B608
98
+ cursor = await conn.execute(by_producer_query, params)
99
+ by_producer_rows = await cursor.fetchall()
100
+ await cursor.close()
101
+ return {row["produced_by"]: row["count"] for row in by_producer_rows}
102
+
103
+ async def group_by_visibility(
104
+ self,
105
+ conn: aiosqlite.Connection,
106
+ where_clause: str,
107
+ params: tuple[Any, ...],
108
+ ) -> dict[str, int]:
109
+ """
110
+ Get artifact counts grouped by visibility kind.
111
+
112
+ Args:
113
+ conn: Database connection
114
+ where_clause: SQL WHERE clause
115
+ params: Parameter values for WHERE clause
116
+
117
+ Returns:
118
+ Dict mapping visibility kinds to counts
119
+ """
120
+ by_visibility_query = f"""
121
+ SELECT json_extract(visibility, '$.kind') AS visibility_kind, COUNT(*) AS count
122
+ FROM artifacts
123
+ {where_clause}
124
+ GROUP BY json_extract(visibility, '$.kind')
125
+ """ # nosec B608
126
+ cursor = await conn.execute(by_visibility_query, params)
127
+ by_visibility_rows = await cursor.fetchall()
128
+ await cursor.close()
129
+ return {
130
+ (row["visibility_kind"] or "Unknown"): row["count"]
131
+ for row in by_visibility_rows
132
+ }
133
+
134
+ async def count_tags(
135
+ self,
136
+ conn: aiosqlite.Connection,
137
+ where_clause: str,
138
+ params: tuple[Any, ...],
139
+ ) -> dict[str, int]:
140
+ """
141
+ Get tag occurrence counts.
142
+
143
+ Args:
144
+ conn: Database connection
145
+ where_clause: SQL WHERE clause
146
+ params: Parameter values for WHERE clause
147
+
148
+ Returns:
149
+ Dict mapping tag names to occurrence counts
150
+ """
151
+ tag_query = f"""
152
+ SELECT json_each.value AS tag, COUNT(*) AS count
153
+ FROM artifacts
154
+ JOIN json_each(artifacts.tags)
155
+ {where_clause}
156
+ GROUP BY json_each.value
157
+ """ # nosec B608
158
+ cursor = await conn.execute(tag_query, params)
159
+ tag_rows = await cursor.fetchall()
160
+ await cursor.close()
161
+ return {row["tag"]: row["count"] for row in tag_rows}
162
+
163
+ async def get_date_range(
164
+ self,
165
+ conn: aiosqlite.Connection,
166
+ where_clause: str,
167
+ params: tuple[Any, ...],
168
+ ) -> tuple[str | None, str | None]:
169
+ """
170
+ Get earliest and latest creation timestamps.
171
+
172
+ Args:
173
+ conn: Database connection
174
+ where_clause: SQL WHERE clause
175
+ params: Parameter values for WHERE clause
176
+
177
+ Returns:
178
+ Tuple of (earliest, latest) ISO timestamp strings, or (None, None)
179
+ """
180
+ range_query = f"""
181
+ SELECT MIN(created_at) AS earliest, MAX(created_at) AS latest
182
+ FROM artifacts
183
+ {where_clause}
184
+ """ # nosec B608
185
+ cursor = await conn.execute(range_query, params)
186
+ range_row = await cursor.fetchone()
187
+ await cursor.close()
188
+
189
+ if not range_row:
190
+ return None, None
191
+
192
+ earliest = range_row["earliest"] if range_row["earliest"] else None
193
+ latest = range_row["latest"] if range_row["latest"] else None
194
+ return earliest, latest
@@ -0,0 +1,14 @@
1
+ """Shared utilities for Flock framework."""
2
+
3
+ from flock.utils.type_resolution import TypeResolutionHelper
4
+ from flock.utils.visibility import VisibilityDeserializer
5
+ from flock.utils.async_utils import async_lock_required, AsyncLockRequired
6
+ from flock.utils.validation import ArtifactValidator
7
+
8
+ __all__ = [
9
+ "TypeResolutionHelper",
10
+ "VisibilityDeserializer",
11
+ "async_lock_required",
12
+ "AsyncLockRequired",
13
+ "ArtifactValidator",
14
+ ]
@@ -0,0 +1,67 @@
1
+ """Async utility decorators and helpers."""
2
+
3
+ from collections.abc import Callable
4
+ from functools import wraps
5
+ from typing import Any, TypeVar
6
+
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class AsyncLockRequired:
12
+ """Decorator ensuring async lock acquisition.
13
+
14
+ This utility eliminates 15+ duplicate lock acquisition patterns
15
+ scattered throughout orchestrator.py and agent.py.
16
+ """
17
+
18
+ def __init__(self, lock_attr: str = "_lock"):
19
+ """
20
+ Initialize decorator.
21
+
22
+ Args:
23
+ lock_attr: Name of lock attribute on class (default: "_lock")
24
+ """
25
+ self.lock_attr = lock_attr
26
+
27
+ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
28
+ """Apply decorator to function."""
29
+ lock_attr = self.lock_attr # Capture in closure
30
+
31
+ @wraps(func)
32
+ async def wrapper(instance: Any, *args: Any, **kwargs: Any) -> Any:
33
+ lock = getattr(instance, lock_attr)
34
+ async with lock:
35
+ return await func(instance, *args, **kwargs)
36
+
37
+ return wrapper
38
+
39
+
40
+ def async_lock_required(lock_attr: str = "_lock") -> AsyncLockRequired:
41
+ """
42
+ Decorator ensuring async lock acquisition.
43
+
44
+ This decorator automatically acquires and releases an async lock
45
+ before executing the decorated method, preventing race conditions.
46
+
47
+ Args:
48
+ lock_attr: Name of the lock attribute on the class (default: "_lock")
49
+
50
+ Returns:
51
+ AsyncLockRequired decorator instance
52
+
53
+ Example:
54
+ >>> class MyClass:
55
+ ... def __init__(self):
56
+ ... self._lock = asyncio.Lock()
57
+ ...
58
+ ... @async_lock_required()
59
+ ... async def my_method(self):
60
+ ... # Lock automatically acquired here
61
+ ... await asyncio.sleep(0.1)
62
+ ... return "done"
63
+
64
+ >>> obj = MyClass()
65
+ >>> result = await obj.my_method() # Lock acquired/released automatically
66
+ """
67
+ return AsyncLockRequired(lock_attr)
@@ -7,7 +7,7 @@ from uuid import UUID
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Field
9
9
 
10
- from flock.artifacts import Artifact
10
+ from flock.core.artifacts import Artifact
11
11
 
12
12
 
13
13
  class EvalInputs(BaseModel):
@@ -99,7 +99,7 @@ class EvalResult(BaseModel):
99
99
  ... )
100
100
  ... return EvalResult.from_object(processed, agent=agent)
101
101
  """
102
- from flock.artifacts import Artifact
102
+ from flock.core.artifacts import Artifact
103
103
  from flock.registry import type_registry
104
104
 
105
105
  type_name = type_registry.name_for(type(obj))
@@ -154,7 +154,7 @@ class EvalResult(BaseModel):
154
154
  ... movie, tagline, agent=agent, metrics={"confidence": 0.9}
155
155
  ... )
156
156
  """
157
- from flock.artifacts import Artifact
157
+ from flock.core.artifacts import Artifact
158
158
  from flock.registry import type_registry
159
159
 
160
160
  artifacts = []
@@ -0,0 +1,53 @@
1
+ """Time formatting utilities.
2
+
3
+ Provides human-readable time span formatting for date ranges.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+
10
+
11
+ def format_time_span(earliest: datetime | None, latest: datetime | None) -> str:
12
+ """
13
+ Format time span between two datetimes as human-readable string.
14
+
15
+ Args:
16
+ earliest: Start datetime
17
+ latest: End datetime
18
+
19
+ Returns:
20
+ Human-readable span description:
21
+ - "X days" for spans >= 2 days
22
+ - "X.Y hours" for spans >= 1 hour
23
+ - "X minutes" for spans > 0
24
+ - "moments" for zero span
25
+ - "empty" if no dates provided
26
+
27
+ Examples:
28
+ >>> from datetime import datetime, timedelta
29
+ >>> now = datetime.now()
30
+ >>> format_time_span(now, now + timedelta(days=3))
31
+ "3 days"
32
+ >>> format_time_span(now, now + timedelta(hours=2))
33
+ "2.0 hours"
34
+ >>> format_time_span(now, now + timedelta(minutes=45))
35
+ "45 minutes"
36
+ """
37
+ if not earliest or not latest:
38
+ return "empty"
39
+
40
+ span = latest - earliest
41
+
42
+ if span.days >= 2:
43
+ return f"{span.days} days"
44
+
45
+ if span.total_seconds() >= 3600:
46
+ hours = span.total_seconds() / 3600
47
+ return f"{hours:.1f} hours"
48
+
49
+ if span.total_seconds() > 0:
50
+ minutes = max(1, int(span.total_seconds() / 60))
51
+ return f"{minutes} minutes"
52
+
53
+ return "moments"
@@ -0,0 +1,38 @@
1
+ """Type registry resolution utilities."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from flock.registry import TypeRegistry
7
+
8
+ from flock.registry import RegistryError
9
+
10
+
11
+ class TypeResolutionHelper:
12
+ """Helper for safe type resolution.
13
+
14
+ This utility eliminates 8+ duplicate type resolution patterns
15
+ scattered across agent.py, store.py, orchestrator.py, and context_provider.py.
16
+ """
17
+
18
+ @staticmethod
19
+ def safe_resolve(registry: "TypeRegistry", type_name: str) -> str:
20
+ """
21
+ Safely resolve type name to canonical form.
22
+
23
+ Args:
24
+ registry: Type registry instance
25
+ type_name: Type name to resolve
26
+
27
+ Returns:
28
+ Canonical type name (or original if not found)
29
+
30
+ Example:
31
+ >>> canonical = TypeResolutionHelper.safe_resolve(registry, "MyType")
32
+ >>> # Returns "my_module.MyType" if found, else "MyType"
33
+ """
34
+ try:
35
+ return registry.resolve_name(type_name)
36
+ except RegistryError:
37
+ # Type not found or ambiguous - return original name
38
+ return type_name
@@ -19,11 +19,11 @@ from rich.pretty import Pretty
19
19
  from rich.table import Table
20
20
  from rich.text import Text
21
21
 
22
- from flock.components import AgentComponent
22
+ from flock.components.agent import AgentComponent
23
23
 
24
24
 
25
25
  if TYPE_CHECKING:
26
- from flock.runtime import Context, EvalInputs, EvalResult
26
+ from flock.utils.runtime import Context, EvalInputs, EvalResult
27
27
 
28
28
 
29
29
  class MetricsUtility(AgentComponent):
@@ -0,0 +1,57 @@
1
+ """Common validation utilities."""
2
+
3
+ from typing import Any, Callable
4
+
5
+ from pydantic import BaseModel, ValidationError
6
+
7
+
8
+ class ArtifactValidator:
9
+ """Validates artifacts against predicates.
10
+
11
+ This utility consolidates artifact validation patterns
12
+ used across agent.py for output validation.
13
+ """
14
+
15
+ @staticmethod
16
+ def validate_artifact(
17
+ artifact: Any,
18
+ model_cls: type[BaseModel],
19
+ predicate: Callable[[BaseModel], bool] | None = None,
20
+ ) -> tuple[bool, BaseModel | None, str | None]:
21
+ """
22
+ Validate artifact payload against model and optional predicate.
23
+
24
+ Args:
25
+ artifact: Artifact to validate
26
+ model_cls: Pydantic model class
27
+ predicate: Optional validation predicate
28
+
29
+ Returns:
30
+ Tuple of (is_valid, model_instance, error_message)
31
+
32
+ Example:
33
+ >>> from pydantic import BaseModel
34
+ >>> class MyModel(BaseModel):
35
+ ... name: str
36
+ ... age: int
37
+ >>> artifact = type("obj", (), {"payload": {"name": "Alice", "age": 30}})()
38
+ >>> is_valid, model, error = ArtifactValidator.validate_artifact(
39
+ ... artifact, MyModel, lambda m: m.age >= 18
40
+ ... )
41
+ >>> assert is_valid
42
+ >>> assert model.name == "Alice"
43
+ """
44
+ try:
45
+ # Validate against model
46
+ model_instance = model_cls(**artifact.payload)
47
+
48
+ # Apply predicate if provided
49
+ if predicate and not predicate(model_instance):
50
+ return False, model_instance, "Predicate validation failed"
51
+
52
+ return True, model_instance, None
53
+
54
+ except ValidationError as e:
55
+ return False, None, str(e)
56
+ except Exception as e:
57
+ return False, None, f"Validation error: {e}"
@@ -0,0 +1,79 @@
1
+ """Visibility deserialization utilities."""
2
+
3
+ from datetime import timedelta
4
+ from typing import Any
5
+
6
+ from flock.core.visibility import (
7
+ AfterVisibility,
8
+ LabelledVisibility,
9
+ PrivateVisibility,
10
+ PublicVisibility,
11
+ TenantVisibility,
12
+ Visibility,
13
+ )
14
+
15
+
16
+ class VisibilityDeserializer:
17
+ """Deserializes visibility from dict/str representation.
18
+
19
+ This utility eliminates 5+ duplicate visibility deserialization patterns
20
+ scattered across store.py, context_provider.py, and orchestrator.py.
21
+ """
22
+
23
+ @staticmethod
24
+ def deserialize(data: dict[str, Any] | str) -> Visibility:
25
+ """
26
+ Deserialize visibility from various formats.
27
+
28
+ Args:
29
+ data: Dict with 'kind' field or string
30
+
31
+ Returns:
32
+ Visibility instance
33
+
34
+ Raises:
35
+ ValueError: If visibility kind is unknown
36
+
37
+ Example:
38
+ >>> vis = VisibilityDeserializer.deserialize({"kind": "Public"})
39
+ >>> assert isinstance(vis, PublicVisibility)
40
+ """
41
+ if isinstance(data, str):
42
+ kind = data
43
+ props = {}
44
+ else:
45
+ kind = data.get("kind")
46
+ props = data
47
+
48
+ if kind == "Public":
49
+ return PublicVisibility()
50
+
51
+ if kind == "Private":
52
+ agents = set(props.get("agents", []))
53
+ return PrivateVisibility(agents=agents)
54
+
55
+ if kind == "Labelled":
56
+ required_labels = set(props.get("required_labels", []))
57
+ return LabelledVisibility(required_labels=required_labels)
58
+
59
+ if kind == "Tenant":
60
+ tenant_id = props.get("tenant_id")
61
+ return TenantVisibility(tenant_id=tenant_id)
62
+
63
+ if kind == "After":
64
+ ttl_value = props.get("ttl")
65
+ # Handle timedelta or raw seconds
66
+ if isinstance(ttl_value, (int, float)):
67
+ ttl = timedelta(seconds=ttl_value)
68
+ elif isinstance(ttl_value, dict):
69
+ # Pydantic dict representation
70
+ ttl = timedelta(**ttl_value)
71
+ else:
72
+ ttl = ttl_value or timedelta()
73
+
74
+ then_data = props.get("then")
75
+ then = VisibilityDeserializer.deserialize(then_data) if then_data else None
76
+
77
+ return AfterVisibility(ttl=ttl, then=then)
78
+
79
+ raise ValueError(f"Unknown visibility kind: {kind}")
@@ -0,0 +1,134 @@
1
+ """Visibility deserialization utilities.
2
+
3
+ This module handles complex visibility object deserialization from JSON data.
4
+ Extracted from store.py to reduce complexity and improve testability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from datetime import timedelta
11
+ from typing import Any
12
+
13
+ from flock.core.visibility import (
14
+ AfterVisibility,
15
+ LabelledVisibility,
16
+ PrivateVisibility,
17
+ PublicVisibility,
18
+ TenantVisibility,
19
+ Visibility,
20
+ )
21
+
22
+
23
+ ISO_DURATION_RE = re.compile(
24
+ r"^P(?:T?(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)$"
25
+ )
26
+
27
+
28
+ def parse_iso_duration(value: str | None) -> timedelta:
29
+ """
30
+ Parse ISO 8601 duration string to timedelta.
31
+
32
+ Args:
33
+ value: ISO 8601 duration string (e.g., "PT1H30M")
34
+
35
+ Returns:
36
+ Parsed timedelta, or zero timedelta if invalid
37
+
38
+ Examples:
39
+ >>> parse_iso_duration("PT1H")
40
+ timedelta(hours=1)
41
+ >>> parse_iso_duration("PT30M")
42
+ timedelta(minutes=30)
43
+ >>> parse_iso_duration(None)
44
+ timedelta(0)
45
+ """
46
+ if not value:
47
+ return timedelta(0)
48
+ match = ISO_DURATION_RE.match(value)
49
+ if not match:
50
+ return timedelta(0)
51
+ hours = int(match.group("hours") or 0)
52
+ minutes = int(match.group("minutes") or 0)
53
+ seconds = int(match.group("seconds") or 0)
54
+ return timedelta(hours=hours, minutes=minutes, seconds=seconds)
55
+
56
+
57
+ def deserialize_visibility(data: Any) -> Visibility:
58
+ """
59
+ Deserialize visibility object from JSON data.
60
+
61
+ Handles all visibility types: Public, Private, Labelled, Tenant, After.
62
+ Uses dictionary dispatch to reduce complexity vs if-elif chain.
63
+
64
+ Args:
65
+ data: JSON data dict or Visibility instance
66
+
67
+ Returns:
68
+ Visibility object (defaults to PublicVisibility if invalid)
69
+
70
+ Examples:
71
+ >>> deserialize_visibility({"kind": "Public"})
72
+ PublicVisibility()
73
+ >>> deserialize_visibility({"kind": "Private", "agents": ["agent1"]})
74
+ PrivateVisibility(agents={"agent1"})
75
+ """
76
+ # Early returns for simple cases
77
+ if isinstance(data, Visibility):
78
+ return data
79
+ if not data:
80
+ return PublicVisibility()
81
+
82
+ # Extract kind
83
+ kind = data.get("kind") if isinstance(data, dict) else None
84
+ if not kind:
85
+ return PublicVisibility()
86
+
87
+ # Dispatch to appropriate deserializer
88
+ return _VISIBILITY_DESERIALIZERS.get(kind, _deserialize_public)(data)
89
+
90
+
91
+ def _deserialize_public(data: dict[str, Any]) -> PublicVisibility:
92
+ """Deserialize PublicVisibility."""
93
+ return PublicVisibility()
94
+
95
+
96
+ def _deserialize_private(data: dict[str, Any]) -> PrivateVisibility:
97
+ """Deserialize PrivateVisibility."""
98
+ return PrivateVisibility(agents=set(data.get("agents", [])))
99
+
100
+
101
+ def _deserialize_labelled(data: dict[str, Any]) -> LabelledVisibility:
102
+ """Deserialize LabelledVisibility."""
103
+ return LabelledVisibility(required_labels=set(data.get("required_labels", [])))
104
+
105
+
106
+ def _deserialize_tenant(data: dict[str, Any]) -> TenantVisibility:
107
+ """Deserialize TenantVisibility."""
108
+ return TenantVisibility(tenant_id=data.get("tenant_id"))
109
+
110
+
111
+ def _deserialize_after(data: dict[str, Any]) -> AfterVisibility:
112
+ """
113
+ Deserialize AfterVisibility with recursive 'then' handling.
114
+
115
+ Args:
116
+ data: JSON data dict with 'ttl' and optional 'then' fields
117
+
118
+ Returns:
119
+ AfterVisibility instance
120
+ """
121
+ ttl = parse_iso_duration(data.get("ttl"))
122
+ then_data = data.get("then") if isinstance(data, dict) else None
123
+ then_visibility = deserialize_visibility(then_data) if then_data else None
124
+ return AfterVisibility(ttl=ttl, then=then_visibility)
125
+
126
+
127
+ # Dispatch table for visibility types
128
+ _VISIBILITY_DESERIALIZERS = {
129
+ "Public": _deserialize_public,
130
+ "Private": _deserialize_private,
131
+ "Labelled": _deserialize_labelled,
132
+ "Tenant": _deserialize_tenant,
133
+ "After": _deserialize_after,
134
+ }