flock-core 0.3.40__py3-none-any.whl → 0.4.0b1__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.
- flock/__init__.py +31 -0
- flock/cli/create_flock.py +58 -3
- flock/cli/load_flock.py +135 -1
- flock/cli/registry_management.py +367 -96
- flock/cli/yaml_editor.py +119 -6
- flock/core/__init__.py +13 -1
- flock/core/flock.py +865 -26
- flock/core/flock_agent.py +114 -22
- flock/core/flock_registry.py +36 -4
- flock/core/serialization/serializable.py +35 -8
- flock/core/util/cli_helper.py +2 -2
- flock/core/util/file_path_utils.py +223 -0
- {flock_core-0.3.40.dist-info → flock_core-0.4.0b1.dist-info}/METADATA +1 -1
- {flock_core-0.3.40.dist-info → flock_core-0.4.0b1.dist-info}/RECORD +17 -16
- {flock_core-0.3.40.dist-info → flock_core-0.4.0b1.dist-info}/WHEEL +0 -0
- {flock_core-0.3.40.dist-info → flock_core-0.4.0b1.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.40.dist-info → flock_core-0.4.0b1.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -374,10 +374,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
374
374
|
mode="json", # Use json mode for better handling of standard types by Pydantic
|
|
375
375
|
exclude_none=True, # Exclude None values for cleaner output
|
|
376
376
|
)
|
|
377
|
+
logger.debug(f"Base agent data for '{self.name}': {list(data.keys())}")
|
|
377
378
|
|
|
378
379
|
# --- Serialize Components using Registry Type Names ---
|
|
379
380
|
# Evaluator
|
|
380
381
|
if self.evaluator:
|
|
382
|
+
logger.debug(f"Serializing evaluator for agent '{self.name}'")
|
|
381
383
|
evaluator_type_name = FlockRegistry.get_component_type_name(
|
|
382
384
|
type(self.evaluator)
|
|
383
385
|
)
|
|
@@ -388,6 +390,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
388
390
|
)
|
|
389
391
|
evaluator_dict["type"] = evaluator_type_name # Add type marker
|
|
390
392
|
data["evaluator"] = evaluator_dict
|
|
393
|
+
logger.debug(
|
|
394
|
+
f"Added evaluator of type '{evaluator_type_name}' to agent '{self.name}'"
|
|
395
|
+
)
|
|
391
396
|
else:
|
|
392
397
|
logger.warning(
|
|
393
398
|
f"Could not get registered type name for evaluator {type(self.evaluator).__name__} in agent '{self.name}'. Skipping serialization."
|
|
@@ -395,6 +400,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
395
400
|
|
|
396
401
|
# Router
|
|
397
402
|
if self.handoff_router:
|
|
403
|
+
logger.debug(f"Serializing router for agent '{self.name}'")
|
|
398
404
|
router_type_name = FlockRegistry.get_component_type_name(
|
|
399
405
|
type(self.handoff_router)
|
|
400
406
|
)
|
|
@@ -406,6 +412,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
406
412
|
)
|
|
407
413
|
router_dict["type"] = router_type_name
|
|
408
414
|
data["handoff_router"] = router_dict
|
|
415
|
+
logger.debug(
|
|
416
|
+
f"Added router of type '{router_type_name}' to agent '{self.name}'"
|
|
417
|
+
)
|
|
409
418
|
else:
|
|
410
419
|
logger.warning(
|
|
411
420
|
f"Could not get registered type name for router {type(self.handoff_router).__name__} in agent '{self.name}'. Skipping serialization."
|
|
@@ -413,6 +422,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
413
422
|
|
|
414
423
|
# Modules
|
|
415
424
|
if self.modules:
|
|
425
|
+
logger.debug(
|
|
426
|
+
f"Serializing {len(self.modules)} modules for agent '{self.name}'"
|
|
427
|
+
)
|
|
416
428
|
serialized_modules = {}
|
|
417
429
|
for name, module_instance in self.modules.items():
|
|
418
430
|
module_type_name = FlockRegistry.get_component_type_name(
|
|
@@ -426,32 +438,55 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
426
438
|
)
|
|
427
439
|
module_dict["type"] = module_type_name
|
|
428
440
|
serialized_modules[name] = module_dict
|
|
441
|
+
logger.debug(
|
|
442
|
+
f"Added module '{name}' of type '{module_type_name}' to agent '{self.name}'"
|
|
443
|
+
)
|
|
429
444
|
else:
|
|
430
445
|
logger.warning(
|
|
431
446
|
f"Could not get registered type name for module {type(module_instance).__name__} ('{name}') in agent '{self.name}'. Skipping."
|
|
432
447
|
)
|
|
433
448
|
if serialized_modules:
|
|
434
449
|
data["modules"] = serialized_modules
|
|
450
|
+
logger.debug(
|
|
451
|
+
f"Added {len(serialized_modules)} modules to agent '{self.name}'"
|
|
452
|
+
)
|
|
435
453
|
|
|
436
454
|
# --- Serialize Tools (Callables) ---
|
|
437
455
|
if self.tools:
|
|
456
|
+
logger.debug(
|
|
457
|
+
f"Serializing {len(self.tools)} tools for agent '{self.name}'"
|
|
458
|
+
)
|
|
438
459
|
serialized_tools = []
|
|
439
460
|
for tool in self.tools:
|
|
440
461
|
if callable(tool) and not isinstance(tool, type):
|
|
441
462
|
path_str = FlockRegistry.get_callable_path_string(tool)
|
|
442
463
|
if path_str:
|
|
443
|
-
|
|
464
|
+
# Get just the function name from the path string
|
|
465
|
+
# If it's a namespaced path like module.submodule.function_name
|
|
466
|
+
# Just use the function_name part
|
|
467
|
+
func_name = path_str.split(".")[-1]
|
|
468
|
+
serialized_tools.append(func_name)
|
|
469
|
+
logger.debug(
|
|
470
|
+
f"Added tool '{func_name}' (from path '{path_str}') to agent '{self.name}'"
|
|
471
|
+
)
|
|
444
472
|
else:
|
|
445
473
|
logger.warning(
|
|
446
474
|
f"Could not get path string for tool {tool} in agent '{self.name}'. Skipping."
|
|
447
475
|
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
476
|
+
else:
|
|
477
|
+
logger.warning(
|
|
478
|
+
f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping."
|
|
479
|
+
)
|
|
451
480
|
if serialized_tools:
|
|
452
481
|
data["tools"] = serialized_tools
|
|
482
|
+
logger.debug(
|
|
483
|
+
f"Added {len(serialized_tools)} tools to agent '{self.name}'"
|
|
484
|
+
)
|
|
453
485
|
|
|
454
486
|
# No need to call _filter_none_values here as model_dump(exclude_none=True) handles it
|
|
487
|
+
logger.info(
|
|
488
|
+
f"Serialization of agent '{self.name}' complete with {len(data)} fields"
|
|
489
|
+
)
|
|
455
490
|
return data
|
|
456
491
|
|
|
457
492
|
@classmethod
|
|
@@ -466,6 +501,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
466
501
|
raise ValueError("Agent data must include a 'name' field.")
|
|
467
502
|
FlockRegistry = get_registry()
|
|
468
503
|
agent_name = data["name"] # For logging context
|
|
504
|
+
logger.info(f"Deserializing agent '{agent_name}'")
|
|
469
505
|
|
|
470
506
|
# Pop complex components to handle them after basic agent instantiation
|
|
471
507
|
evaluator_data = data.pop("evaluator", None)
|
|
@@ -473,6 +509,10 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
473
509
|
modules_data = data.pop("modules", {})
|
|
474
510
|
tools_data = data.pop("tools", [])
|
|
475
511
|
|
|
512
|
+
logger.debug(
|
|
513
|
+
f"Agent '{agent_name}' has {len(modules_data)} modules and {len(tools_data)} tools"
|
|
514
|
+
)
|
|
515
|
+
|
|
476
516
|
# Deserialize remaining data recursively (handles nested basic types/callables)
|
|
477
517
|
# Note: Pydantic v2 handles most basic deserialization well if types match.
|
|
478
518
|
# Explicit deserialize_item might be needed if complex non-pydantic structures exist.
|
|
@@ -481,6 +521,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
481
521
|
|
|
482
522
|
try:
|
|
483
523
|
# Create the agent instance using Pydantic's constructor
|
|
524
|
+
logger.debug(
|
|
525
|
+
f"Creating agent instance with fields: {list(deserialized_basic_data.keys())}"
|
|
526
|
+
)
|
|
484
527
|
agent = cls(**deserialized_basic_data)
|
|
485
528
|
except Exception as e:
|
|
486
529
|
logger.error(
|
|
@@ -495,13 +538,16 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
495
538
|
# Evaluator
|
|
496
539
|
if evaluator_data:
|
|
497
540
|
try:
|
|
541
|
+
logger.debug(
|
|
542
|
+
f"Deserializing evaluator for agent '{agent_name}'"
|
|
543
|
+
)
|
|
498
544
|
agent.evaluator = deserialize_component(
|
|
499
545
|
evaluator_data, FlockEvaluator
|
|
500
546
|
)
|
|
501
547
|
if agent.evaluator is None:
|
|
502
548
|
raise ValueError("deserialize_component returned None")
|
|
503
549
|
logger.debug(
|
|
504
|
-
f"Deserialized evaluator '{agent.evaluator.name}' for agent '{agent_name}'"
|
|
550
|
+
f"Deserialized evaluator '{agent.evaluator.name}' of type '{evaluator_data.get('type')}' for agent '{agent_name}'"
|
|
505
551
|
)
|
|
506
552
|
except Exception as e:
|
|
507
553
|
logger.error(
|
|
@@ -514,13 +560,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
514
560
|
# Router
|
|
515
561
|
if router_data:
|
|
516
562
|
try:
|
|
563
|
+
logger.debug(f"Deserializing router for agent '{agent_name}'")
|
|
517
564
|
agent.handoff_router = deserialize_component(
|
|
518
565
|
router_data, FlockRouter
|
|
519
566
|
)
|
|
520
567
|
if agent.handoff_router is None:
|
|
521
568
|
raise ValueError("deserialize_component returned None")
|
|
522
569
|
logger.debug(
|
|
523
|
-
f"Deserialized router '{agent.handoff_router.name}' for agent '{agent_name}'"
|
|
570
|
+
f"Deserialized router '{agent.handoff_router.name}' of type '{router_data.get('type')}' for agent '{agent_name}'"
|
|
524
571
|
)
|
|
525
572
|
except Exception as e:
|
|
526
573
|
logger.error(
|
|
@@ -532,8 +579,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
532
579
|
# Modules
|
|
533
580
|
if modules_data:
|
|
534
581
|
agent.modules = {} # Ensure it's initialized
|
|
582
|
+
logger.debug(
|
|
583
|
+
f"Deserializing {len(modules_data)} modules for agent '{agent_name}'"
|
|
584
|
+
)
|
|
535
585
|
for name, module_data in modules_data.items():
|
|
536
586
|
try:
|
|
587
|
+
logger.debug(
|
|
588
|
+
f"Deserializing module '{name}' of type '{module_data.get('type')}' for agent '{agent_name}'"
|
|
589
|
+
)
|
|
537
590
|
module_instance = deserialize_component(
|
|
538
591
|
module_data, FlockModule
|
|
539
592
|
)
|
|
@@ -543,6 +596,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
543
596
|
agent.add_module(
|
|
544
597
|
module_instance
|
|
545
598
|
) # Use add_module for consistency
|
|
599
|
+
logger.debug(
|
|
600
|
+
f"Successfully added module '{name}' to agent '{agent_name}'"
|
|
601
|
+
)
|
|
546
602
|
else:
|
|
547
603
|
raise ValueError("deserialize_component returned None")
|
|
548
604
|
except Exception as e:
|
|
@@ -555,25 +611,61 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
555
611
|
# --- Deserialize Tools ---
|
|
556
612
|
agent.tools = [] # Initialize tools list
|
|
557
613
|
if tools_data:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
614
|
+
# Get component registry to look up function imports
|
|
615
|
+
registry = get_registry()
|
|
616
|
+
components = getattr(registry, "_callables", {})
|
|
617
|
+
logger.debug(
|
|
618
|
+
f"Deserializing {len(tools_data)} tools for agent '{agent_name}'"
|
|
619
|
+
)
|
|
620
|
+
logger.debug(
|
|
621
|
+
f"Available callables in registry: {list(components.keys())}"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
for tool_name in tools_data:
|
|
625
|
+
try:
|
|
626
|
+
logger.debug(f"Looking for tool '{tool_name}' in registry")
|
|
627
|
+
# First try to lookup by simple name in the registry's callables
|
|
628
|
+
found = False
|
|
629
|
+
for path_str, func in components.items():
|
|
630
|
+
if (
|
|
631
|
+
path_str.endswith("." + tool_name)
|
|
632
|
+
or path_str == tool_name
|
|
633
|
+
):
|
|
634
|
+
agent.tools.append(func)
|
|
635
|
+
found = True
|
|
636
|
+
logger.info(
|
|
637
|
+
f"Found tool '{tool_name}' via path '{path_str}' for agent '{agent_name}'"
|
|
638
|
+
)
|
|
639
|
+
break
|
|
640
|
+
|
|
641
|
+
# If not found by simple name, try manual import
|
|
642
|
+
if not found:
|
|
643
|
+
logger.debug(
|
|
644
|
+
f"Attempting to import tool '{tool_name}' from modules"
|
|
570
645
|
)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
646
|
+
# Check in relevant modules (could be customized based on project structure)
|
|
647
|
+
import __main__
|
|
648
|
+
|
|
649
|
+
if hasattr(__main__, tool_name):
|
|
650
|
+
agent.tools.append(getattr(__main__, tool_name))
|
|
651
|
+
found = True
|
|
652
|
+
logger.info(
|
|
653
|
+
f"Found tool '{tool_name}' in __main__ module for agent '{agent_name}'"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if not found:
|
|
657
|
+
logger.warning(
|
|
658
|
+
f"Could not find tool '{tool_name}' for agent '{agent_name}'"
|
|
659
|
+
)
|
|
660
|
+
except Exception as e:
|
|
661
|
+
logger.error(
|
|
662
|
+
f"Error adding tool '{tool_name}' to agent '{agent_name}': {e}",
|
|
663
|
+
exc_info=True,
|
|
574
664
|
)
|
|
575
665
|
|
|
576
|
-
logger.info(
|
|
666
|
+
logger.info(
|
|
667
|
+
f"Successfully deserialized agent '{agent_name}' with {len(agent.modules)} modules and {len(agent.tools)} tools"
|
|
668
|
+
)
|
|
577
669
|
return agent
|
|
578
670
|
|
|
579
671
|
# --- Pydantic v2 Configuration ---
|
flock/core/flock_registry.py
CHANGED
|
@@ -162,16 +162,20 @@ class FlockRegistry:
|
|
|
162
162
|
and self._callables[path_str] != func
|
|
163
163
|
):
|
|
164
164
|
logger.warning(
|
|
165
|
-
f"Callable '{path_str}' already registered. Overwriting."
|
|
165
|
+
f"Callable '{path_str}' already registered with a different function. Overwriting."
|
|
166
166
|
)
|
|
167
167
|
self._callables[path_str] = func
|
|
168
|
-
logger.debug(f"Registered callable: {path_str}")
|
|
168
|
+
logger.debug(f"Registered callable: '{path_str}' ({func.__name__})")
|
|
169
169
|
return path_str
|
|
170
|
+
logger.warning(
|
|
171
|
+
f"Could not register callable {func.__name__}: Unable to determine path string"
|
|
172
|
+
)
|
|
170
173
|
return None
|
|
171
174
|
|
|
172
175
|
def get_callable(self, path_str: str) -> Callable:
|
|
173
176
|
"""Retrieves a callable by its path string, attempting dynamic import if not found."""
|
|
174
177
|
if path_str in self._callables:
|
|
178
|
+
logger.debug(f"Found callable '{path_str}' in registry")
|
|
175
179
|
return self._callables[path_str]
|
|
176
180
|
|
|
177
181
|
logger.debug(
|
|
@@ -179,14 +183,20 @@ class FlockRegistry:
|
|
|
179
183
|
)
|
|
180
184
|
try:
|
|
181
185
|
if "." not in path_str: # Built-ins
|
|
186
|
+
logger.debug(f"Trying to import built-in callable '{path_str}'")
|
|
182
187
|
builtins_module = importlib.import_module("builtins")
|
|
183
188
|
if hasattr(builtins_module, path_str):
|
|
184
189
|
func = getattr(builtins_module, path_str)
|
|
185
190
|
if callable(func):
|
|
186
191
|
self.register_callable(func, path_str) # Cache it
|
|
192
|
+
logger.info(
|
|
193
|
+
f"Successfully imported built-in callable '{path_str}'"
|
|
194
|
+
)
|
|
187
195
|
return func
|
|
196
|
+
logger.error(f"Built-in callable '{path_str}' not found.")
|
|
188
197
|
raise KeyError(f"Built-in callable '{path_str}' not found.")
|
|
189
198
|
|
|
199
|
+
logger.debug(f"Trying to import module callable '{path_str}'")
|
|
190
200
|
module_name, func_name = path_str.rsplit(".", 1)
|
|
191
201
|
module = importlib.import_module(module_name)
|
|
192
202
|
func = getattr(module, func_name)
|
|
@@ -194,14 +204,21 @@ class FlockRegistry:
|
|
|
194
204
|
self.register_callable(
|
|
195
205
|
func, path_str
|
|
196
206
|
) # Cache dynamically imported
|
|
207
|
+
logger.info(
|
|
208
|
+
f"Successfully imported module callable '{path_str}'"
|
|
209
|
+
)
|
|
197
210
|
return func
|
|
198
211
|
else:
|
|
212
|
+
logger.error(
|
|
213
|
+
f"Dynamically imported object '{path_str}' is not callable."
|
|
214
|
+
)
|
|
199
215
|
raise TypeError(
|
|
200
216
|
f"Dynamically imported object '{path_str}' is not callable."
|
|
201
217
|
)
|
|
202
218
|
except (ImportError, AttributeError, KeyError, TypeError) as e:
|
|
203
219
|
logger.error(
|
|
204
|
-
f"Failed to dynamically load/find callable '{path_str}': {e}"
|
|
220
|
+
f"Failed to dynamically load/find callable '{path_str}': {e}",
|
|
221
|
+
exc_info=True,
|
|
205
222
|
)
|
|
206
223
|
raise KeyError(
|
|
207
224
|
f"Callable '{path_str}' not found or failed to load: {e}"
|
|
@@ -209,11 +226,26 @@ class FlockRegistry:
|
|
|
209
226
|
|
|
210
227
|
def get_callable_path_string(self, func: Callable) -> str | None:
|
|
211
228
|
"""Gets the path string for a callable, registering it if necessary."""
|
|
229
|
+
# First try to find by direct identity
|
|
212
230
|
for path_str, registered_func in self._callables.items():
|
|
213
231
|
if func == registered_func:
|
|
232
|
+
logger.debug(
|
|
233
|
+
f"Found existing path string for callable: '{path_str}'"
|
|
234
|
+
)
|
|
214
235
|
return path_str
|
|
236
|
+
|
|
215
237
|
# If not found by identity, generate path, register, and return
|
|
216
|
-
|
|
238
|
+
path_str = self.register_callable(func)
|
|
239
|
+
if path_str:
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"Generated and registered new path string for callable: '{path_str}'"
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
logger.warning(
|
|
245
|
+
f"Failed to generate path string for callable {func.__name__}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return path_str
|
|
217
249
|
|
|
218
250
|
# --- Type Registration ---
|
|
219
251
|
def register_type(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import json
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, TypeVar
|
|
5
|
+
from typing import Any, Literal, TypeVar
|
|
6
6
|
|
|
7
7
|
# Use yaml if available, otherwise skip yaml methods
|
|
8
8
|
try:
|
|
@@ -85,16 +85,32 @@ class Serializable(ABC):
|
|
|
85
85
|
) from e
|
|
86
86
|
|
|
87
87
|
# --- YAML Methods ---
|
|
88
|
-
def to_yaml(
|
|
89
|
-
|
|
88
|
+
def to_yaml(
|
|
89
|
+
self,
|
|
90
|
+
path_type: Literal["absolute", "relative"],
|
|
91
|
+
sort_keys=False,
|
|
92
|
+
default_flow_style=False,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Serialize to YAML string.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
98
|
+
sort_keys: Whether to sort dictionary keys
|
|
99
|
+
default_flow_style: YAML flow style setting
|
|
100
|
+
"""
|
|
90
101
|
if not YAML_AVAILABLE:
|
|
91
102
|
raise NotImplementedError(
|
|
92
103
|
"YAML support requires PyYAML: pip install pyyaml"
|
|
93
104
|
)
|
|
94
105
|
try:
|
|
95
|
-
# to_dict
|
|
106
|
+
# If to_dict supports path_type, pass it; otherwise use standard to_dict
|
|
107
|
+
if "path_type" in self.to_dict.__code__.co_varnames:
|
|
108
|
+
dict_data = self.to_dict(path_type=path_type)
|
|
109
|
+
else:
|
|
110
|
+
dict_data = self.to_dict()
|
|
111
|
+
|
|
96
112
|
return yaml.dump(
|
|
97
|
-
|
|
113
|
+
dict_data,
|
|
98
114
|
sort_keys=sort_keys,
|
|
99
115
|
default_flow_style=default_flow_style,
|
|
100
116
|
allow_unicode=True,
|
|
@@ -125,8 +141,19 @@ class Serializable(ABC):
|
|
|
125
141
|
f"Failed to deserialize {cls.__name__} from YAML: {e}"
|
|
126
142
|
) from e
|
|
127
143
|
|
|
128
|
-
def to_yaml_file(
|
|
129
|
-
|
|
144
|
+
def to_yaml_file(
|
|
145
|
+
self,
|
|
146
|
+
path: Path | str,
|
|
147
|
+
path_type: Literal["absolute", "relative"] = "absolute",
|
|
148
|
+
**yaml_dump_kwargs,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Serialize to YAML file.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
path: File path to write to
|
|
154
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
155
|
+
**yaml_dump_kwargs: Additional arguments to pass to yaml.dump
|
|
156
|
+
"""
|
|
130
157
|
if not YAML_AVAILABLE:
|
|
131
158
|
raise NotImplementedError(
|
|
132
159
|
"YAML support requires PyYAML: pip install pyyaml"
|
|
@@ -134,7 +161,7 @@ class Serializable(ABC):
|
|
|
134
161
|
path = Path(path)
|
|
135
162
|
try:
|
|
136
163
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
-
yaml_str = self.to_yaml(**yaml_dump_kwargs)
|
|
164
|
+
yaml_str = self.to_yaml(path_type=path_type, **yaml_dump_kwargs)
|
|
138
165
|
path.write_text(yaml_str, encoding="utf-8")
|
|
139
166
|
except Exception as e:
|
|
140
167
|
raise RuntimeError(
|
flock/core/util/cli_helper.py
CHANGED
|
@@ -52,7 +52,7 @@ def init_console(clear_screen: bool = True):
|
|
|
52
52
|
│ ▒█▀▀▀ █░░ █▀▀█ █▀▀ █░█ │
|
|
53
53
|
│ ▒█▀▀▀ █░░ █░░█ █░░ █▀▄ │
|
|
54
54
|
│ ▒█░░░ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀░▀ │
|
|
55
|
-
|
|
55
|
+
╰━━━━━━━━v{__version__}━━━━━━━━╯
|
|
56
56
|
🦆 🐤 🐧 🐓
|
|
57
57
|
""",
|
|
58
58
|
justify="center",
|
|
@@ -62,7 +62,7 @@ def init_console(clear_screen: bool = True):
|
|
|
62
62
|
console.clear()
|
|
63
63
|
console.print(banner_text)
|
|
64
64
|
console.print(
|
|
65
|
-
f"[italic]'
|
|
65
|
+
f"[italic]'Magpie'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Utility functions for handling file paths in Flock.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for working with file paths,
|
|
4
|
+
especially for components that may be loaded from file system paths
|
|
5
|
+
rather than module imports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import inspect
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_file_path(obj: Any) -> str | None:
|
|
17
|
+
"""Get the file path for a Python object.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
obj: The object to get the file path for
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The file path if it can be determined, None otherwise
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
if inspect.ismodule(obj):
|
|
27
|
+
return obj.__file__
|
|
28
|
+
elif inspect.isclass(obj) or inspect.isfunction(obj):
|
|
29
|
+
return inspect.getfile(obj)
|
|
30
|
+
return None
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_path(path: str) -> str:
|
|
36
|
+
"""Normalize a path for consistent representation.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path: The path to normalize
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The normalized path
|
|
43
|
+
"""
|
|
44
|
+
return os.path.normpath(path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_same_path(path1: str, path2: str) -> bool:
|
|
48
|
+
"""Check if two paths point to the same file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path1: The first path
|
|
52
|
+
path2: The second path
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if the paths point to the same file, False otherwise
|
|
56
|
+
"""
|
|
57
|
+
return os.path.normpath(os.path.abspath(path1)) == os.path.normpath(
|
|
58
|
+
os.path.abspath(path2)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_relative_path(path: str, base_path: str | None = None) -> str:
|
|
63
|
+
"""Get a path relative to a base path.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: The path to make relative
|
|
67
|
+
base_path: The base path (defaults to current working directory)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The relative path
|
|
71
|
+
"""
|
|
72
|
+
if base_path is None:
|
|
73
|
+
base_path = os.getcwd()
|
|
74
|
+
|
|
75
|
+
return os.path.relpath(path, base_path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_class_from_file(file_path: str, class_name: str) -> type | None:
|
|
79
|
+
"""Load a class from a file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
file_path: The path to the file
|
|
83
|
+
class_name: The name of the class to load
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The loaded class, or None if it could not be loaded
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
# Generate a unique module name to avoid conflicts
|
|
90
|
+
module_name = f"flock_dynamic_import_{hash(file_path)}"
|
|
91
|
+
|
|
92
|
+
# Create a spec for the module
|
|
93
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
94
|
+
if not spec or not spec.loader:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Create and load the module
|
|
98
|
+
module = importlib.util.module_from_spec(spec)
|
|
99
|
+
sys.modules[module_name] = module
|
|
100
|
+
spec.loader.exec_module(module)
|
|
101
|
+
|
|
102
|
+
# Get the class from the module
|
|
103
|
+
if not hasattr(module, class_name):
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
return getattr(module, class_name)
|
|
107
|
+
except Exception:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_project_root() -> Path:
|
|
112
|
+
"""Get the project root directory.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The project root path
|
|
116
|
+
"""
|
|
117
|
+
# Try to find the directory containing pyproject.toml or setup.py
|
|
118
|
+
current_dir = Path(os.getcwd())
|
|
119
|
+
|
|
120
|
+
# Walk up the directory tree looking for project markers
|
|
121
|
+
for path in [current_dir, *current_dir.parents]:
|
|
122
|
+
if (path / "pyproject.toml").exists() or (path / "setup.py").exists():
|
|
123
|
+
return path
|
|
124
|
+
|
|
125
|
+
# Default to current directory if no project markers found
|
|
126
|
+
return current_dir
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def component_path_to_file_path(component_path: str) -> str | None:
|
|
130
|
+
"""Convert a component path (module.ClassName) to a file path.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
component_path: The component path in the form module.ClassName
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The file path if it can be determined, None otherwise
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
# Split into module path and class name
|
|
140
|
+
if "." not in component_path:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
module_path, class_name = component_path.rsplit(".", 1)
|
|
144
|
+
|
|
145
|
+
# Import the module
|
|
146
|
+
module = importlib.import_module(module_path)
|
|
147
|
+
|
|
148
|
+
# Get the file path
|
|
149
|
+
if hasattr(module, "__file__"):
|
|
150
|
+
return module.__file__
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
except (ImportError, AttributeError):
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def file_path_to_component_path(file_path: str, class_name: str) -> str | None:
|
|
158
|
+
"""Convert a file path and class name to a component path (module.ClassName).
|
|
159
|
+
|
|
160
|
+
This is approximate and may not work in all cases, especially for non-standard
|
|
161
|
+
module structures.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
file_path: The file path to the module
|
|
165
|
+
class_name: The name of the class
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The component path if it can be determined, None otherwise
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# Convert the file path to an absolute path
|
|
172
|
+
abs_path = os.path.abspath(file_path)
|
|
173
|
+
|
|
174
|
+
# Get the project root
|
|
175
|
+
root = get_project_root()
|
|
176
|
+
|
|
177
|
+
# Get the relative path from the project root
|
|
178
|
+
rel_path = os.path.relpath(abs_path, root)
|
|
179
|
+
|
|
180
|
+
# Convert to a module path
|
|
181
|
+
module_path = os.path.splitext(rel_path)[0].replace(os.sep, ".")
|
|
182
|
+
|
|
183
|
+
# Remove 'src.' prefix if present (common in Python projects)
|
|
184
|
+
if module_path.startswith("src."):
|
|
185
|
+
module_path = module_path[4:]
|
|
186
|
+
|
|
187
|
+
# Combine with the class name
|
|
188
|
+
return f"{module_path}.{class_name}"
|
|
189
|
+
except Exception:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def register_file_paths_in_registry(
|
|
194
|
+
component_paths: dict[str, str], registry: Any | None = None
|
|
195
|
+
) -> bool:
|
|
196
|
+
"""Register file paths in the registry.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
component_paths: Dictionary mapping component paths to file paths
|
|
200
|
+
registry: The registry to register in (defaults to the global registry)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if all paths were registered, False otherwise
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
# Get the global registry if none provided
|
|
207
|
+
if registry is None:
|
|
208
|
+
from flock.core.flock_registry import get_registry
|
|
209
|
+
|
|
210
|
+
registry = get_registry()
|
|
211
|
+
|
|
212
|
+
# Initialize component_file_paths if needed
|
|
213
|
+
if not hasattr(registry, "_component_file_paths"):
|
|
214
|
+
registry._component_file_paths = {}
|
|
215
|
+
|
|
216
|
+
# Register each path
|
|
217
|
+
for component_name, file_path in component_paths.items():
|
|
218
|
+
if component_name in registry._components:
|
|
219
|
+
registry._component_file_paths[component_name] = file_path
|
|
220
|
+
|
|
221
|
+
return True
|
|
222
|
+
except Exception:
|
|
223
|
+
return False
|