sequential-thinking 0.6.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.
@@ -0,0 +1,152 @@
1
+ import threading
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from .logging_conf import configure_logging
6
+ from .models import ThoughtData, ThoughtStage
7
+ from .storage_utils import (
8
+ load_thoughts_from_file,
9
+ prepare_thoughts_for_serialization,
10
+ save_thoughts_to_file,
11
+ )
12
+
13
+ logger = configure_logging("sequential-thinking.storage")
14
+
15
+
16
+ class ThoughtStorage:
17
+ """Storage manager for thought data."""
18
+
19
+ def __init__(self, storage_dir: str | None = None):
20
+ """Initialize the storage manager.
21
+
22
+ Args:
23
+ storage_dir: Directory to store thought data files. If None, uses a default directory.
24
+ """
25
+ if storage_dir is None:
26
+ # Use user's home directory by default
27
+ home_dir = Path.home()
28
+ self.storage_dir = home_dir / ".mcp_sequential_thinking"
29
+ else:
30
+ self.storage_dir = Path(storage_dir)
31
+
32
+ # Create storage directory if it doesn't exist
33
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ # Default session file
36
+ self.current_session_file = self.storage_dir / "current_session.json"
37
+ self.lock_file = self.storage_dir / "current_session.lock"
38
+
39
+ # Thread safety
40
+ self._lock = threading.RLock()
41
+ self.thought_history: list[ThoughtData] = []
42
+
43
+ # Load existing session if available
44
+ self._load_session()
45
+
46
+ def _load_session(self) -> None:
47
+ """Load thought history from the current session file if it exists."""
48
+ with self._lock:
49
+ # Use the utility function to handle loading with proper error handling
50
+ self.thought_history = load_thoughts_from_file(
51
+ self.current_session_file, self.lock_file
52
+ )
53
+
54
+ def _save_session(self) -> None:
55
+ """Save the current thought history to the session file."""
56
+ # Use thread lock to ensure consistent data
57
+ with self._lock:
58
+ # Use utility functions to prepare and save thoughts
59
+ thoughts_with_ids = prepare_thoughts_for_serialization(self.thought_history)
60
+
61
+ # Save to file with proper locking
62
+ save_thoughts_to_file(self.current_session_file, thoughts_with_ids, self.lock_file)
63
+
64
+ def add_thought(self, thought: ThoughtData) -> None:
65
+ """Add a thought to the history and save the session.
66
+
67
+ Args:
68
+ thought: The thought data to add
69
+ """
70
+ with self._lock:
71
+ self.thought_history.append(thought)
72
+ self._save_session()
73
+
74
+ def get_all_thoughts(self) -> list[ThoughtData]:
75
+ """Get all thoughts in the current session.
76
+
77
+ Returns:
78
+ List[ThoughtData]: All thoughts in the current session
79
+ """
80
+ with self._lock:
81
+ # Return a copy to avoid external modification
82
+ return list(self.thought_history)
83
+
84
+ def get_thoughts_by_stage(self, stage: ThoughtStage) -> list[ThoughtData]:
85
+ """Get all thoughts in a specific stage.
86
+
87
+ Args:
88
+ stage: The thinking stage to filter by
89
+
90
+ Returns:
91
+ List[ThoughtData]: Thoughts in the specified stage
92
+ """
93
+ with self._lock:
94
+ return [t for t in self.thought_history if t.stage == stage]
95
+
96
+ def clear_history(self) -> None:
97
+ """Clear the thought history and save the empty session."""
98
+ with self._lock:
99
+ self.thought_history.clear()
100
+ self._save_session()
101
+
102
+ def export_session(self, file_path: str) -> None:
103
+ """Export the current session to a file.
104
+
105
+ Args:
106
+ file_path: Path to save the exported session
107
+ """
108
+ with self._lock:
109
+ # Use utility function to prepare thoughts for serialization
110
+ thoughts_with_ids = prepare_thoughts_for_serialization(self.thought_history)
111
+
112
+ # Create export-specific metadata
113
+ metadata = {
114
+ "exportedAt": datetime.now().isoformat(),
115
+ "metadata": {
116
+ "totalThoughts": len(self.thought_history),
117
+ "stages": {
118
+ stage.value: len([t for t in self.thought_history if t.stage == stage])
119
+ for stage in ThoughtStage
120
+ },
121
+ },
122
+ }
123
+
124
+ # Convert string path to Path object for compatibility with utility
125
+ file_path_obj = Path(file_path)
126
+ lock_file = file_path_obj.with_suffix(".lock")
127
+
128
+ # Use utility function to save with proper locking
129
+ save_thoughts_to_file(file_path_obj, thoughts_with_ids, lock_file, metadata)
130
+
131
+ def import_session(self, file_path: str) -> None:
132
+ """Import a session from a file.
133
+
134
+ Args:
135
+ file_path: Path to the file to import
136
+
137
+ Raises:
138
+ FileNotFoundError: If the file doesn't exist
139
+ json.JSONDecodeError: If the file is not valid JSON
140
+ KeyError: If the file doesn't contain valid thought data
141
+ """
142
+ # Convert string path to Path object for compatibility with utility
143
+ file_path_obj = Path(file_path)
144
+ lock_file = file_path_obj.with_suffix(".lock")
145
+
146
+ # Use utility function to load thoughts with proper error handling
147
+ thoughts = load_thoughts_from_file(file_path_obj, lock_file)
148
+
149
+ with self._lock:
150
+ self.thought_history = thoughts
151
+
152
+ self._save_session()
@@ -0,0 +1,107 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Any, cast
5
+
6
+ import portalocker
7
+
8
+ from .logging_conf import configure_logging
9
+ from .models import ThoughtData
10
+
11
+ logger = configure_logging("sequential-thinking.storage-utils")
12
+
13
+
14
+ def prepare_thoughts_for_serialization(thoughts: list[ThoughtData]) -> list[dict[str, Any]]:
15
+ """Prepare thoughts for serialization with IDs included.
16
+
17
+ Args:
18
+ thoughts: List of thought data objects to prepare
19
+
20
+ Returns:
21
+ List[Dict[str, Any]]: List of thought dictionaries with IDs
22
+ """
23
+ return [thought.to_dict(include_id=True) for thought in thoughts]
24
+
25
+
26
+ def save_thoughts_to_file(
27
+ file_path: Path,
28
+ thoughts: list[dict[str, Any]],
29
+ lock_file: Path,
30
+ metadata: dict[str, Any] | None = None,
31
+ ) -> None:
32
+ """Save thoughts to a file with proper locking.
33
+
34
+ Args:
35
+ file_path: Path to the file to save
36
+ thoughts: List of thought dictionaries to save
37
+ lock_file: Path to the lock file
38
+ metadata: Optional additional metadata to include
39
+ """
40
+ data: dict[str, Any] = {"thoughts": thoughts, "lastUpdated": datetime.now().isoformat()}
41
+
42
+ # Add any additional metadata if provided
43
+ if metadata:
44
+ data.update(metadata)
45
+
46
+ # Ensure destination directories exist before acquiring the lock.
47
+ file_path.parent.mkdir(parents=True, exist_ok=True)
48
+ lock_file.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ # Use file locking to ensure thread safety when writing
51
+ with portalocker.Lock(lock_file, timeout=10), open(file_path, "w", encoding="utf-8") as f:
52
+ json.dump(data, f, indent=2, ensure_ascii=False)
53
+
54
+ logger.debug(f"Saved {len(thoughts)} thoughts to {file_path}")
55
+
56
+
57
+ def load_thoughts_from_file(file_path: Path, lock_file: Path) -> list[ThoughtData]:
58
+ """Load thoughts from a file with proper locking.
59
+
60
+ Args:
61
+ file_path: Path to the file to load
62
+ lock_file: Path to the lock file
63
+
64
+ Returns:
65
+ List[ThoughtData]: Loaded thought data objects
66
+
67
+ Raises:
68
+ json.JSONDecodeError: If the file is not valid JSON
69
+ KeyError: If the file doesn't contain valid thought data
70
+ """
71
+ if not file_path.exists():
72
+ return []
73
+
74
+ try:
75
+ # Use file locking and file handling in a single with statement
76
+ # for cleaner resource management
77
+ with portalocker.Lock(lock_file, timeout=10), open(file_path, encoding="utf-8") as f:
78
+ data = json.load(f)
79
+
80
+ if not isinstance(data, dict):
81
+ return []
82
+
83
+ data_dict = cast(dict[str, Any], data)
84
+ raw_thoughts = data_dict.get("thoughts", [])
85
+ if not isinstance(raw_thoughts, list):
86
+ return []
87
+
88
+ thought_dicts = cast(list[Any], raw_thoughts)
89
+
90
+ # Convert data to ThoughtData objects after file is closed
91
+ thoughts = [
92
+ ThoughtData.from_dict(cast(dict[str, Any], thought_dict))
93
+ for thought_dict in thought_dicts
94
+ if isinstance(thought_dict, dict)
95
+ ]
96
+
97
+ logger.debug(f"Loaded {len(thoughts)} thoughts from {file_path}")
98
+ return thoughts
99
+
100
+ except (json.JSONDecodeError, KeyError) as e:
101
+ # Handle corrupted file
102
+ logger.error(f"Error loading from {file_path}: {e}")
103
+ # Create backup of corrupted file
104
+ backup_file = file_path.with_suffix(f".bak.{datetime.now().strftime('%Y%m%d%H%M%S')}")
105
+ file_path.rename(backup_file)
106
+ logger.info(f"Created backup of corrupted file at {backup_file}")
107
+ return []
@@ -0,0 +1,71 @@
1
+ """Utility functions for the sequential thinking package.
2
+
3
+ This module contains common utilities used across the package.
4
+ """
5
+
6
+ import re
7
+ from collections.abc import Callable
8
+ from typing import Any, cast
9
+
10
+
11
+ def to_camel_case(snake_str: str) -> str:
12
+ """Convert a snake_case string to camelCase.
13
+
14
+ Args:
15
+ snake_str: A string in snake_case format
16
+
17
+ Returns:
18
+ The string converted to camelCase
19
+ """
20
+ components = snake_str.split("_")
21
+ # Join with the first component lowercase and the rest with their first letter capitalized
22
+ return components[0] + "".join(x.title() for x in components[1:])
23
+
24
+
25
+ def to_snake_case(camel_str: str) -> str:
26
+ """Convert a camelCase string to snake_case.
27
+
28
+ Args:
29
+ camel_str: A string in camelCase format
30
+
31
+ Returns:
32
+ The string converted to snake_case
33
+ """
34
+ # Insert underscore before uppercase letters and convert to lowercase
35
+ s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", camel_str)
36
+ return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
37
+
38
+
39
+ def convert_dict_keys(data: Any, converter: Callable[[str], str]) -> Any:
40
+ """Convert all keys in a dictionary using the provided converter function.
41
+
42
+ Args:
43
+ data: Dictionary with keys to convert
44
+ converter: Function to convert the keys (e.g. to_camel_case or to_snake_case)
45
+
46
+ Returns:
47
+ A new dictionary with converted keys
48
+ """
49
+ if not isinstance(data, dict):
50
+ return data
51
+
52
+ source = cast(dict[str, Any], data)
53
+ result: dict[str, Any] = {}
54
+ for key, value in source.items():
55
+ # Convert key
56
+ new_key = converter(key)
57
+
58
+ # If value is a dict, recursively convert its keys too
59
+ if isinstance(value, dict):
60
+ result[new_key] = convert_dict_keys(value, converter)
61
+ # If value is a list, check if items are dicts and convert them
62
+ elif isinstance(value, list):
63
+ items = cast(list[Any], value)
64
+ result[new_key] = [
65
+ convert_dict_keys(item, converter) if isinstance(item, dict) else item
66
+ for item in items
67
+ ]
68
+ else:
69
+ result[new_key] = value
70
+
71
+ return result