mrradix commited on
Commit
972a3bd
·
verified ·
1 Parent(s): afc51ae

Update utils/state.py

Browse files
Files changed (1) hide show
  1. utils/state.py +263 -232
utils/state.py CHANGED
@@ -1,296 +1,327 @@
1
- from typing import Dict, Any, List, Optional
2
- import datetime
3
  import uuid
4
- from pathlib import Path
 
 
 
5
 
6
- from utils.config import DATA_DIR, DEFAULT_SETTINGS
7
- from utils.storage import load_data, save_data
8
  from utils.logging import get_logger
9
  from utils.error_handling import handle_exceptions, ValidationError, safe_get
 
10
 
11
- # Initialize logger
12
  logger = get_logger(__name__)
13
 
14
- def generate_id() -> str:
 
 
 
 
 
15
  """
16
  Generate a unique ID
17
 
 
 
 
18
  Returns:
19
- A UUID string
20
  """
21
- return str(uuid.uuid4())
 
 
 
22
 
23
- def get_timestamp() -> str:
 
24
  """
25
- Get current timestamp in ISO format
 
 
 
26
 
27
  Returns:
28
- Current timestamp as ISO formatted string
29
  """
30
- return datetime.datetime.now().isoformat()
 
 
 
 
 
 
 
 
 
31
 
32
  @handle_exceptions
33
- def initialize_state() -> Dict[str, Any]:
34
  """
35
- Initialize the application state with default values
 
 
 
 
36
 
37
  Returns:
38
- Dictionary containing the application state
39
-
40
- Raises:
41
- DataError: If there's an error loading data files
42
  """
43
- logger.info("Initializing application state")
44
-
45
- # Create data directory if it doesn't exist
46
- DATA_DIR.mkdir(exist_ok=True, parents=True)
47
-
48
- # Load existing data or create defaults
49
- logger.debug("Loading data files")
50
- tasks = load_data(DATA_DIR / "tasks.json", default=[])
51
- notes = load_data(DATA_DIR / "notes.json", default=[])
52
- goals = load_data(DATA_DIR / "goals.json", default=[])
53
- settings = load_data(DATA_DIR / "settings.json", default=DEFAULT_SETTINGS)
54
-
55
- # Calculate stats
56
- tasks_total = len(tasks)
57
- tasks_completed = sum(1 for task in tasks if safe_get(task, "completed", False))
58
- notes_total = len(notes)
59
- goals_total = len(goals)
60
- goals_completed = sum(1 for goal in goals if safe_get(goal, "completed", False))
61
- streak_days = calculate_streak_days()
62
-
63
- logger.info(f"Loaded {tasks_total} tasks, {notes_total} notes, {goals_total} goals")
64
-
65
- # Initialize state
66
- state = {
67
- # Data
68
- "tasks": tasks,
69
- "notes": notes,
70
- "goals": goals,
71
- "settings": settings,
72
 
73
- # Session state
74
- "current_page": "🏠 Dashboard",
75
- "selected_task": None,
76
- "selected_note": None,
77
- "selected_goal": None,
78
 
79
- # UI state containers (will be populated during app creation)
80
- "page_containers": {},
 
81
 
82
- # Stats
83
- "stats": {
84
- "tasks_total": tasks_total,
85
- "tasks_completed": tasks_completed,
86
- "notes_total": notes_total,
87
- "goals_total": goals_total,
88
- "goals_completed": goals_completed,
89
- "streak_days": streak_days
90
- },
91
 
92
- # Activity feed
93
- "activity_feed": load_recent_activity()
94
- }
95
-
96
- logger.info("Application state initialized successfully")
97
- return state
98
 
99
  @handle_exceptions
100
- def calculate_streak_days() -> int:
101
  """
102
- Calculate the current streak of consecutive days using the app
 
 
 
 
103
 
104
  Returns:
105
- Number of consecutive days
106
-
107
- Raises:
108
- DataError: If there's an error loading usage data
 
 
109
  """
110
- logger.debug("Calculating streak days")
111
- usage_file = DATA_DIR / "usage.json"
112
- usage_data = load_data(usage_file, default=[])
113
 
114
- if not usage_data:
115
- return 0
 
116
 
117
- # Extract dates from usage data
 
 
118
  try:
119
- dates = [datetime.datetime.fromisoformat(safe_get(entry, "timestamp", "")).date()
120
- for entry in usage_data if "timestamp" in entry]
121
- except (ValueError, TypeError) as e:
122
- logger.warning(f"Error parsing dates in usage data: {str(e)}")
123
- return 0
124
-
125
- # Sort and get unique dates
126
- unique_dates = sorted(set(dates), reverse=True)
127
-
128
- if not unique_dates:
129
- return 0
130
-
131
- # Calculate streak
132
- streak = 1
133
- today = datetime.date.today()
134
 
135
- # If no usage today, check if there was usage yesterday to continue the streak
136
- if unique_dates[0] != today:
137
- if unique_dates[0] != today - datetime.timedelta(days=1):
138
- return 0
139
- reference_date = unique_dates[0]
140
- else:
141
- reference_date = today
142
 
143
- # Count consecutive days
144
- for i in range(1, len(unique_dates)):
145
- expected_date = reference_date - datetime.timedelta(days=i)
146
- if expected_date in unique_dates:
147
- streak += 1
 
 
 
148
  else:
149
- break
150
-
151
- logger.debug(f"Current streak: {streak} days")
152
- return streak
 
153
 
154
  @handle_exceptions
155
- def load_recent_activity(limit: int = 20) -> List[Dict[str, Any]]:
156
  """
157
- Load recent activity from various data sources
158
 
159
  Args:
160
- limit: Maximum number of activities to return
161
-
162
  Returns:
163
- List of activity entries
164
-
165
- Raises:
166
- DataError: If there's an error loading data files
167
- ValidationError: If limit is not a positive integer
168
  """
169
- if not isinstance(limit, int) or limit <= 0:
170
- logger.error(f"Invalid limit for load_recent_activity: {limit}")
171
- raise ValidationError(f"Limit must be a positive integer, got {limit}")
172
-
173
- logger.debug(f"Loading recent activity (limit: {limit})")
174
- activities = []
175
-
176
- # Load tasks
177
- tasks = load_data(DATA_DIR / "tasks.json", default=[])
178
- for task in tasks:
179
- if "created_at" in task:
180
- activities.append({
181
- "timestamp": safe_get(task, "created_at", ""),
182
- "type": "task_created",
183
- "title": safe_get(task, "title", "Untitled Task"),
184
- "id": safe_get(task, "id", "")
185
- })
186
- if "completed_at" in task and safe_get(task, "completed", False):
187
- activities.append({
188
- "timestamp": safe_get(task, "completed_at", ""),
189
- "type": "task_completed",
190
- "title": safe_get(task, "title", "Untitled Task"),
191
- "id": safe_get(task, "id", "")
192
- })
193
-
194
- # Load notes
195
- notes = load_data(DATA_DIR / "notes.json", default=[])
196
- for note in notes:
197
- if "created_at" in note:
198
- activities.append({
199
- "timestamp": safe_get(note, "created_at", ""),
200
- "type": "note_created",
201
- "title": safe_get(note, "title", "Untitled Note"),
202
- "id": safe_get(note, "id", "")
203
- })
204
- if "updated_at" in note and safe_get(note, "updated_at", "") != safe_get(note, "created_at", ""):
205
- activities.append({
206
- "timestamp": safe_get(note, "updated_at", ""),
207
- "type": "note_updated",
208
- "title": safe_get(note, "title", "Untitled Note"),
209
- "id": safe_get(note, "id", "")
210
- })
211
 
212
- # Load goals
213
- goals = load_data(DATA_DIR / "goals.json", default=[])
214
- for goal in goals:
215
- if "created_at" in goal:
216
- activities.append({
217
- "timestamp": safe_get(goal, "created_at", ""),
218
- "type": "goal_created",
219
- "title": safe_get(goal, "title", "Untitled Goal"),
220
- "id": safe_get(goal, "id", "")
221
- })
222
- if "completed_at" in goal and safe_get(goal, "completed", False):
223
- activities.append({
224
- "timestamp": safe_get(goal, "completed_at", ""),
225
- "type": "goal_completed",
226
- "title": safe_get(goal, "title", "Untitled Goal"),
227
- "id": safe_get(goal, "id", "")
228
- })
229
 
230
- # Sort by timestamp (newest first) and limit
 
 
231
  try:
232
- activities.sort(key=lambda x: x["timestamp"], reverse=True)
233
- except (KeyError, TypeError) as e:
234
- logger.warning(f"Error sorting activities: {str(e)}")
235
-
236
- logger.debug(f"Loaded {len(activities)} activities")
237
- return activities[:limit]
238
 
239
  @handle_exceptions
240
- def record_activity(state: Dict[str, Any], activity_type: str, title: str,
241
- item_id: Optional[str] = None) -> None:
242
  """
243
- Record a new activity in the activity feed
244
 
245
  Args:
246
- state: Application state
247
- activity_type: Type of activity
248
- title: Title of the related item
249
- item_id: ID of the related item
250
-
251
- Raises:
252
- DataError: If there's an error saving activity data
253
- ValidationError: If required parameters are missing or invalid
254
  """
255
- if not state:
256
- logger.error("Cannot record activity: state is empty or None")
257
- raise ValidationError("Application state is required")
 
 
 
 
258
 
259
- if not activity_type:
260
- logger.error("Cannot record activity: activity_type is empty or None")
261
- raise ValidationError("Activity type is required")
262
 
263
- if not title:
264
- logger.warning("Recording activity with empty title")
265
- title = "Untitled"
266
-
267
- timestamp = get_timestamp()
268
- logger.debug(f"Recording activity: {activity_type} - {title} at {timestamp}")
 
 
269
 
270
- new_activity = {
271
- "timestamp": timestamp,
272
- "type": activity_type,
273
- "title": title,
274
- "id": item_id
275
- }
276
 
277
- # Add to state
278
- if "activity_feed" not in state:
279
- state["activity_feed"] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- state["activity_feed"].insert(0, new_activity)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
- # Limit to 20 items
284
- state["activity_feed"] = state["activity_feed"][:20]
 
285
 
286
- # Save to file
287
- activities = load_data(DATA_DIR / "activities.json", default=[])
288
- activities.insert(0, new_activity)
289
- activities = activities[:100] # Keep last 100 activities
290
- save_data(DATA_DIR / "activities.json", activities)
 
 
 
 
 
 
 
 
 
 
 
291
 
292
- # Log the activity - commented out due to incompatible parameters
293
- # The log_user_activity function requires user_id as first parameter
294
- # log_user_activity(activity_type, {"title": title, "id": item_id})
295
 
296
- logger.debug("Activity recorded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import uuid
2
+ import time
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Dict, List, Optional, Union
5
+ import json
6
 
7
+ # Import utilities
 
8
  from utils.logging import get_logger
9
  from utils.error_handling import handle_exceptions, ValidationError, safe_get
10
+ from utils.storage import save_data, load_data
11
 
 
12
  logger = get_logger(__name__)
13
 
14
+ # Global state storage
15
+ _state_storage = {}
16
+ _activity_log = []
17
+
18
+ @handle_exceptions
19
+ def generate_id(prefix: str = "") -> str:
20
  """
21
  Generate a unique ID
22
 
23
+ Args:
24
+ prefix: Optional prefix for the ID
25
+
26
  Returns:
27
+ str: Unique identifier
28
  """
29
+ unique_id = str(uuid.uuid4())[:8]
30
+ if prefix:
31
+ return f"{prefix}_{unique_id}"
32
+ return unique_id
33
 
34
+ @handle_exceptions
35
+ def get_timestamp(format_type: str = "iso") -> str:
36
  """
37
+ Get current timestamp
38
+
39
+ Args:
40
+ format_type: Format type ('iso', 'unix', 'readable')
41
 
42
  Returns:
43
+ str: Formatted timestamp
44
  """
45
+ now = datetime.now(timezone.utc)
46
+
47
+ if format_type == "iso":
48
+ return now.isoformat()
49
+ elif format_type == "unix":
50
+ return str(int(now.timestamp()))
51
+ elif format_type == "readable":
52
+ return now.strftime("%Y-%m-%d %H:%M:%S UTC")
53
+ else:
54
+ return now.isoformat()
55
 
56
  @handle_exceptions
57
+ def record_activity(activity_type: str, details: Dict[str, Any] = None) -> bool:
58
  """
59
+ Record an activity in the activity log
60
+
61
+ Args:
62
+ activity_type: Type of activity
63
+ details: Additional details about the activity
64
 
65
  Returns:
66
+ bool: True if recorded successfully
 
 
 
67
  """
68
+ try:
69
+ activity = {
70
+ "id": generate_id("activity"),
71
+ "type": activity_type,
72
+ "timestamp": get_timestamp(),
73
+ "details": details or {}
74
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ _activity_log.append(activity)
 
 
 
 
77
 
78
+ # Keep only last 1000 activities
79
+ if len(_activity_log) > 1000:
80
+ _activity_log.pop(0)
81
 
82
+ logger.info(f"Activity recorded: {activity_type}")
83
+ return True
 
 
 
 
 
 
 
84
 
85
+ except Exception as e:
86
+ logger.error(f"Failed to record activity: {e}")
87
+ return False
 
 
 
88
 
89
  @handle_exceptions
90
+ def get_state(key: str, default: Any = None) -> Any:
91
  """
92
+ Get value from global state
93
+
94
+ Args:
95
+ key: State key
96
+ default: Default value if key not found
97
 
98
  Returns:
99
+ Any: State value or default
100
+ """
101
+ return safe_get(_state_storage, key, default)
102
+
103
+ @handle_exceptions
104
+ def set_state(key: str, value: Any) -> bool:
105
  """
106
+ Set value in global state
 
 
107
 
108
+ Args:
109
+ key: State key
110
+ value: Value to set
111
 
112
+ Returns:
113
+ bool: True if set successfully
114
+ """
115
  try:
116
+ _state_storage[key] = value
117
+ logger.debug(f"State set: {key}")
118
+ return True
119
+ except Exception as e:
120
+ logger.error(f"Failed to set state {key}: {e}")
121
+ return False
122
+
123
+ @handle_exceptions
124
+ def update_state(key: str, updates: Dict[str, Any]) -> bool:
125
+ """
126
+ Update nested state values
 
 
 
 
127
 
128
+ Args:
129
+ key: State key
130
+ updates: Dictionary of updates to apply
 
 
 
 
131
 
132
+ Returns:
133
+ bool: True if updated successfully
134
+ """
135
+ try:
136
+ current = get_state(key, {})
137
+ if isinstance(current, dict):
138
+ current.update(updates)
139
+ return set_state(key, current)
140
  else:
141
+ logger.warning(f"Cannot update non-dict state: {key}")
142
+ return False
143
+ except Exception as e:
144
+ logger.error(f"Failed to update state {key}: {e}")
145
+ return False
146
 
147
  @handle_exceptions
148
+ def clear_state(key: Optional[str] = None) -> bool:
149
  """
150
+ Clear state (specific key or all)
151
 
152
  Args:
153
+ key: Specific key to clear, or None to clear all
154
+
155
  Returns:
156
+ bool: True if cleared successfully
 
 
 
 
157
  """
158
+ try:
159
+ if key:
160
+ if key in _state_storage:
161
+ del _state_storage[key]
162
+ logger.info(f"State cleared: {key}")
163
+ else:
164
+ _state_storage.clear()
165
+ logger.info("All state cleared")
166
+ return True
167
+ except Exception as e:
168
+ logger.error(f"Failed to clear state: {e}")
169
+ return False
170
+
171
+ @handle_exceptions
172
+ def get_activity_log(limit: int = 100) -> List[Dict[str, Any]]:
173
+ """
174
+ Get recent activity log entries
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ Args:
177
+ limit: Maximum number of entries to return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ Returns:
180
+ List[Dict]: Activity log entries
181
+ """
182
  try:
183
+ return _activity_log[-limit:] if _activity_log else []
184
+ except Exception as e:
185
+ logger.error(f"Failed to get activity log: {e}")
186
+ return []
 
 
187
 
188
  @handle_exceptions
189
+ def save_state_to_file(filename: str = "app_state.json") -> bool:
 
190
  """
191
+ Save current state to file
192
 
193
  Args:
194
+ filename: Name of the file to save to
195
+
196
+ Returns:
197
+ bool: True if saved successfully
 
 
 
 
198
  """
199
+ try:
200
+ state_data = {
201
+ "state": _state_storage,
202
+ "activity_log": _activity_log[-100:], # Save last 100 activities
203
+ "timestamp": get_timestamp(),
204
+ "version": "1.0"
205
+ }
206
 
207
+ return save_data(state_data, filename, "json")
 
 
208
 
209
+ except Exception as e:
210
+ logger.error(f"Failed to save state to file: {e}")
211
+ return False
212
+
213
+ @handle_exceptions
214
+ def load_state_from_file(filename: str = "app_state.json") -> bool:
215
+ """
216
+ Load state from file
217
 
218
+ Args:
219
+ filename: Name of the file to load from
 
 
 
 
220
 
221
+ Returns:
222
+ bool: True if loaded successfully
223
+ """
224
+ try:
225
+ global _state_storage, _activity_log
226
+
227
+ state_data = load_data(filename, "json")
228
+ if state_data:
229
+ _state_storage = safe_get(state_data, "state", {})
230
+ _activity_log = safe_get(state_data, "activity_log", [])
231
+
232
+ logger.info("State loaded from file successfully")
233
+ record_activity("state_loaded", {"filename": filename})
234
+ return True
235
+ else:
236
+ logger.warning(f"No state file found: {filename}")
237
+ return False
238
+
239
+ except Exception as e:
240
+ logger.error(f"Failed to load state from file: {e}")
241
+ return False
242
+
243
+ @handle_exceptions
244
+ def get_state_info() -> Dict[str, Any]:
245
+ """
246
+ Get information about current state
247
 
248
+ Returns:
249
+ Dict: State information
250
+ """
251
+ try:
252
+ return {
253
+ "state_keys": list(_state_storage.keys()),
254
+ "state_size": len(_state_storage),
255
+ "activity_count": len(_activity_log),
256
+ "last_activity": _activity_log[-1] if _activity_log else None,
257
+ "timestamp": get_timestamp()
258
+ }
259
+ except Exception as e:
260
+ logger.error(f"Failed to get state info: {e}")
261
+ return {}
262
+
263
+ # Session state management for Streamlit compatibility
264
+ @handle_exceptions
265
+ def init_session_state():
266
+ """
267
+ Initialize session state for Streamlit
268
+ """
269
+ try:
270
+ import streamlit as st
271
+
272
+ if 'mona_initialized' not in st.session_state:
273
+ st.session_state.mona_initialized = True
274
+ st.session_state.mona_state = {}
275
+ record_activity("session_initialized")
276
+
277
+ except ImportError:
278
+ # Not in Streamlit environment
279
+ pass
280
+ except Exception as e:
281
+ logger.error(f"Failed to initialize session state: {e}")
282
+
283
+ @handle_exceptions
284
+ def get_session_state(key: str, default: Any = None) -> Any:
285
+ """
286
+ Get value from Streamlit session state
287
 
288
+ Args:
289
+ key: Session state key
290
+ default: Default value
291
 
292
+ Returns:
293
+ Any: Session state value or default
294
+ """
295
+ try:
296
+ import streamlit as st
297
+ return st.session_state.get(key, default)
298
+ except ImportError:
299
+ return get_state(key, default)
300
+ except Exception as e:
301
+ logger.error(f"Failed to get session state {key}: {e}")
302
+ return default
303
+
304
+ @handle_exceptions
305
+ def set_session_state(key: str, value: Any) -> bool:
306
+ """
307
+ Set value in Streamlit session state
308
 
309
+ Args:
310
+ key: Session state key
311
+ value: Value to set
312
 
313
+ Returns:
314
+ bool: True if set successfully
315
+ """
316
+ try:
317
+ import streamlit as st
318
+ st.session_state[key] = value
319
+ return True
320
+ except ImportError:
321
+ return set_state(key, value)
322
+ except Exception as e:
323
+ logger.error(f"Failed to set session state {key}: {e}")
324
+ return False
325
+
326
+ # Initialize on module import
327
+ record_activity("state_module_loaded")