wappa 0.1.8__py3-none-any.whl → 0.1.10__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 wappa might be problematic. Click here for more details.

Files changed (147) hide show
  1. wappa/__init__.py +4 -5
  2. wappa/api/controllers/webhook_controller.py +5 -2
  3. wappa/api/dependencies/__init__.py +0 -5
  4. wappa/api/middleware/error_handler.py +4 -4
  5. wappa/api/middleware/owner.py +11 -5
  6. wappa/api/routes/webhooks.py +2 -2
  7. wappa/cli/__init__.py +1 -1
  8. wappa/cli/examples/init/.env.example +33 -0
  9. wappa/cli/examples/init/app/__init__.py +0 -0
  10. wappa/cli/examples/init/app/main.py +9 -0
  11. wappa/cli/examples/init/app/master_event.py +10 -0
  12. wappa/cli/examples/json_cache_example/.env.example +33 -0
  13. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  14. wappa/cli/examples/json_cache_example/app/main.py +247 -0
  15. wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
  16. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  17. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
  18. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  19. wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
  20. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
  21. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
  22. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
  23. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
  24. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  25. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
  26. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
  27. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  28. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  29. wappa/cli/examples/openai_transcript/app/main.py +9 -0
  30. wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
  31. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  32. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
  33. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  34. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  35. wappa/cli/examples/redis_cache_example/app/main.py +246 -0
  36. wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
  37. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
  38. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  39. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
  40. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
  41. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
  42. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
  43. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
  44. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  45. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
  46. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
  47. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  48. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  49. wappa/cli/examples/simple_echo_example/app/main.py +191 -0
  50. wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
  51. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  52. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  53. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  54. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  55. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
  56. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
  57. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
  58. wappa/cli/examples/wappa_full_example/app/main.py +269 -0
  59. wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
  60. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  61. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  62. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  63. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  64. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  65. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  66. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  67. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  68. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  69. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  70. wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
  71. wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
  72. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
  73. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  74. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
  75. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
  76. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
  77. wappa/cli/main.py +14 -5
  78. wappa/core/__init__.py +18 -23
  79. wappa/core/config/settings.py +7 -5
  80. wappa/core/events/default_handlers.py +1 -1
  81. wappa/core/factory/wappa_builder.py +38 -25
  82. wappa/core/plugins/redis_plugin.py +1 -3
  83. wappa/core/plugins/wappa_core_plugin.py +7 -6
  84. wappa/core/types.py +12 -12
  85. wappa/core/wappa_app.py +10 -8
  86. wappa/database/__init__.py +3 -4
  87. wappa/domain/enums/messenger_platform.py +1 -2
  88. wappa/domain/factories/media_factory.py +5 -20
  89. wappa/domain/factories/message_factory.py +5 -20
  90. wappa/domain/factories/messenger_factory.py +2 -4
  91. wappa/domain/interfaces/cache_interface.py +7 -7
  92. wappa/domain/interfaces/media_interface.py +2 -5
  93. wappa/domain/models/media_result.py +1 -3
  94. wappa/domain/models/platforms/platform_config.py +1 -3
  95. wappa/messaging/__init__.py +9 -12
  96. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  97. wappa/models/__init__.py +27 -35
  98. wappa/persistence/__init__.py +12 -15
  99. wappa/persistence/cache_factory.py +0 -1
  100. wappa/persistence/json/__init__.py +1 -1
  101. wappa/persistence/json/cache_adapters.py +37 -25
  102. wappa/persistence/json/handlers/state_handler.py +60 -52
  103. wappa/persistence/json/handlers/table_handler.py +51 -49
  104. wappa/persistence/json/handlers/user_handler.py +71 -55
  105. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  106. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  107. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  108. wappa/persistence/json/json_cache_factory.py +4 -8
  109. wappa/persistence/json/storage_manager.py +66 -79
  110. wappa/persistence/memory/__init__.py +1 -1
  111. wappa/persistence/memory/cache_adapters.py +37 -25
  112. wappa/persistence/memory/handlers/state_handler.py +62 -52
  113. wappa/persistence/memory/handlers/table_handler.py +59 -53
  114. wappa/persistence/memory/handlers/user_handler.py +75 -55
  115. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  116. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  117. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  118. wappa/persistence/memory/memory_cache_factory.py +3 -7
  119. wappa/persistence/memory/storage_manager.py +52 -62
  120. wappa/persistence/redis/cache_adapters.py +27 -21
  121. wappa/persistence/redis/ops.py +11 -11
  122. wappa/persistence/redis/redis_client.py +4 -6
  123. wappa/persistence/redis/redis_manager.py +12 -4
  124. wappa/processors/factory.py +5 -5
  125. wappa/schemas/factory.py +2 -5
  126. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  127. wappa/schemas/whatsapp/validators.py +3 -3
  128. wappa/webhooks/__init__.py +17 -18
  129. wappa/webhooks/factory.py +3 -5
  130. wappa/webhooks/whatsapp/__init__.py +10 -13
  131. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  132. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  133. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  134. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  135. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  136. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  137. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  138. wappa/webhooks/whatsapp/status_models.py +2 -2
  139. wappa/webhooks/whatsapp/validators.py +3 -3
  140. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  141. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
  142. wappa/cli/examples/init/pyproject.toml +0 -7
  143. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  144. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  145. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  146. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  147. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -5,9 +5,9 @@ Provides user-specific cache operations using JSON file storage.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Any, Dict, Optional
8
+ from typing import Any
9
9
 
10
- from pydantic import BaseModel, Field
10
+ from pydantic import BaseModel
11
11
 
12
12
  from ..storage_manager import storage_manager
13
13
  from .utils.key_factory import default_key_factory
@@ -18,196 +18,212 @@ logger = logging.getLogger("JSONUser")
18
18
  class JSONUser:
19
19
  """
20
20
  JSON-based user cache handler.
21
-
21
+
22
22
  Mirrors RedisUser functionality using file-based JSON storage.
23
23
  Maintains the same API for seamless cache backend switching.
24
24
  """
25
-
25
+
26
26
  def __init__(self, tenant: str, user_id: str):
27
27
  """
28
28
  Initialize JSON user handler.
29
-
29
+
30
30
  Args:
31
31
  tenant: Tenant identifier
32
32
  user_id: User identifier
33
33
  """
34
34
  if not tenant or not user_id:
35
- raise ValueError(f"Missing required parameters: tenant={tenant}, user_id={user_id}")
36
-
35
+ raise ValueError(
36
+ f"Missing required parameters: tenant={tenant}, user_id={user_id}"
37
+ )
38
+
37
39
  self.tenant = tenant
38
40
  self.user_id = user_id
39
41
  self.keys = default_key_factory
40
-
42
+
41
43
  def _key(self) -> str:
42
44
  """Build user key using KeyFactory (same as Redis)."""
43
45
  return self.keys.user(self.tenant, self.user_id)
44
-
46
+
45
47
  # ---- Public API matching RedisUser ----
46
48
  async def get(self, models: type[BaseModel] | None = None) -> dict[str, Any] | None:
47
49
  """
48
50
  Get full user data.
49
-
51
+
50
52
  Args:
51
53
  models: Optional BaseModel class for deserialization
52
-
54
+
53
55
  Returns:
54
56
  User data dictionary or BaseModel instance, None if not found
55
57
  """
56
58
  key = self._key()
57
- return await storage_manager.get("users", self.tenant, self.user_id, key, models)
58
-
59
- async def upsert(self, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
59
+ return await storage_manager.get(
60
+ "users", self.tenant, self.user_id, key, models
61
+ )
62
+
63
+ async def upsert(
64
+ self, data: dict[str, Any] | BaseModel, ttl: int | None = None
65
+ ) -> bool:
60
66
  """
61
67
  Create or update user data.
62
-
68
+
63
69
  Args:
64
70
  data: User data to store
65
71
  ttl: Time to live in seconds
66
-
72
+
67
73
  Returns:
68
74
  True if successful, False otherwise
69
75
  """
70
76
  key = self._key()
71
- return await storage_manager.set("users", self.tenant, self.user_id, key, data, ttl)
72
-
77
+ return await storage_manager.set(
78
+ "users", self.tenant, self.user_id, key, data, ttl
79
+ )
80
+
73
81
  async def delete(self) -> int:
74
82
  """
75
83
  Delete user data.
76
-
84
+
77
85
  Returns:
78
86
  1 if deleted, 0 if didn't exist
79
87
  """
80
88
  key = self._key()
81
89
  success = await storage_manager.delete("users", self.tenant, self.user_id, key)
82
90
  return 1 if success else 0
83
-
91
+
84
92
  async def exists(self) -> bool:
85
93
  """
86
94
  Check if user data exists.
87
-
95
+
88
96
  Returns:
89
97
  True if exists, False otherwise
90
98
  """
91
99
  key = self._key()
92
100
  return await storage_manager.exists("users", self.tenant, self.user_id, key)
93
-
101
+
94
102
  async def get_field(self, field: str) -> Any | None:
95
103
  """
96
104
  Get a specific field from user data.
97
-
105
+
98
106
  Args:
99
107
  field: Field name
100
-
108
+
101
109
  Returns:
102
110
  Field value or None if not found
103
111
  """
104
112
  user_data = await self.get()
105
113
  if user_data is None:
106
114
  return None
107
-
115
+
108
116
  if isinstance(user_data, dict):
109
117
  return user_data.get(field)
110
118
  else:
111
119
  # BaseModel instance
112
120
  return getattr(user_data, field, None)
113
-
114
- async def update_field(self, field: str, value: Any, ttl: int | None = None) -> bool:
121
+
122
+ async def update_field(
123
+ self, field: str, value: Any, ttl: int | None = None
124
+ ) -> bool:
115
125
  """
116
126
  Update a specific field in user data.
117
-
127
+
118
128
  Args:
119
129
  field: Field name
120
130
  value: New value
121
131
  ttl: Time to live in seconds
122
-
132
+
123
133
  Returns:
124
134
  True if successful, False otherwise
125
135
  """
126
136
  user_data = await self.get()
127
137
  if user_data is None:
128
138
  user_data = {}
129
-
139
+
130
140
  if isinstance(user_data, BaseModel):
131
141
  user_data = user_data.model_dump()
132
-
142
+
133
143
  user_data[field] = value
134
144
  return await self.upsert(user_data, ttl)
135
-
136
- async def increment_field(self, field: str, increment: int = 1, ttl: int | None = None) -> int | None:
145
+
146
+ async def increment_field(
147
+ self, field: str, increment: int = 1, ttl: int | None = None
148
+ ) -> int | None:
137
149
  """
138
150
  Atomically increment an integer field.
139
-
151
+
140
152
  Args:
141
153
  field: Field name
142
154
  increment: Amount to increment by
143
155
  ttl: Time to live in seconds
144
-
156
+
145
157
  Returns:
146
158
  New value after increment or None on error
147
159
  """
148
160
  user_data = await self.get()
149
161
  if user_data is None:
150
162
  user_data = {}
151
-
163
+
152
164
  if isinstance(user_data, BaseModel):
153
165
  user_data = user_data.model_dump()
154
-
166
+
155
167
  current_value = user_data.get(field, 0)
156
- if not isinstance(current_value, (int, float)):
157
- logger.warning(f"Cannot increment non-numeric field '{field}': {current_value}")
168
+ if not isinstance(current_value, int | float):
169
+ logger.warning(
170
+ f"Cannot increment non-numeric field '{field}': {current_value}"
171
+ )
158
172
  return None
159
-
173
+
160
174
  new_value = int(current_value) + increment
161
175
  user_data[field] = new_value
162
-
176
+
163
177
  success = await self.upsert(user_data, ttl)
164
178
  return new_value if success else None
165
-
166
- async def append_to_list(self, field: str, value: Any, ttl: int | None = None) -> bool:
179
+
180
+ async def append_to_list(
181
+ self, field: str, value: Any, ttl: int | None = None
182
+ ) -> bool:
167
183
  """
168
184
  Append value to a list field.
169
-
185
+
170
186
  Args:
171
187
  field: Field name containing list
172
188
  value: Value to append
173
189
  ttl: Time to live in seconds
174
-
190
+
175
191
  Returns:
176
192
  True if successful, False otherwise
177
193
  """
178
194
  user_data = await self.get()
179
195
  if user_data is None:
180
196
  user_data = {}
181
-
197
+
182
198
  if isinstance(user_data, BaseModel):
183
199
  user_data = user_data.model_dump()
184
-
200
+
185
201
  current_list = user_data.get(field, [])
186
202
  if not isinstance(current_list, list):
187
203
  current_list = []
188
-
204
+
189
205
  current_list.append(value)
190
206
  user_data[field] = current_list
191
-
207
+
192
208
  return await self.upsert(user_data, ttl)
193
-
209
+
194
210
  async def get_ttl(self, key: str) -> int:
195
211
  """
196
212
  Get remaining time to live.
197
-
213
+
198
214
  Returns:
199
215
  Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
200
216
  """
201
217
  return await storage_manager.get_ttl("users", self.tenant, self.user_id)
202
-
218
+
203
219
  async def renew_ttl(self, key: str, ttl: int) -> bool:
204
220
  """
205
221
  Renew time to live.
206
-
222
+
207
223
  Args:
208
224
  ttl: New time to live in seconds
209
-
225
+
210
226
  Returns:
211
227
  True if successful, False otherwise
212
228
  """
213
- return await storage_manager.set_ttl("users", self.tenant, self.user_id, ttl)
229
+ return await storage_manager.set_ttl("users", self.tenant, self.user_id, ttl)
@@ -7,9 +7,8 @@ Handles cache directory creation, file I/O, and project root detection.
7
7
  import asyncio
8
8
  import json
9
9
  import logging
10
- import os
11
10
  from pathlib import Path
12
- from typing import Any, Dict, Optional
11
+ from typing import Any
13
12
 
14
13
  from .serialization import from_json_string, to_json_string
15
14
 
@@ -18,76 +17,80 @@ logger = logging.getLogger("JSONFileManager")
18
17
 
19
18
  class FileManager:
20
19
  """Manages file operations for JSON cache."""
21
-
20
+
22
21
  def __init__(self):
23
- self._cache_root: Optional[Path] = None
24
- self._file_locks: Dict[str, asyncio.Lock] = {}
25
-
22
+ self._cache_root: Path | None = None
23
+ self._file_locks: dict[str, asyncio.Lock] = {}
24
+
26
25
  def _get_file_lock(self, file_path: str) -> asyncio.Lock:
27
26
  """Get or create a lock for a specific file path."""
28
27
  if file_path not in self._file_locks:
29
28
  self._file_locks[file_path] = asyncio.Lock()
30
29
  return self._file_locks[file_path]
31
-
30
+
32
31
  def get_cache_root(self) -> Path:
33
32
  """Get or detect the cache root directory."""
34
33
  if self._cache_root is None:
35
34
  self._cache_root = self._detect_project_root()
36
35
  return self._cache_root
37
-
36
+
38
37
  def _detect_project_root(self) -> Path:
39
38
  """
40
39
  Detect project root by looking for main.py with Wappa.run().
41
-
40
+
42
41
  Searches from current working directory upwards.
43
42
  Falls back to current directory if not found.
44
43
  """
45
44
  current_dir = Path.cwd()
46
-
45
+
47
46
  # Search upwards for main.py containing Wappa.run()
48
47
  for directory in [current_dir] + list(current_dir.parents):
49
48
  main_py = directory / "main.py"
50
49
  if main_py.exists():
51
50
  try:
52
- content = main_py.read_text(encoding='utf-8')
53
- if "Wappa" in content and (".run()" in content or "app.run()" in content):
51
+ content = main_py.read_text(encoding="utf-8")
52
+ if "Wappa" in content and (
53
+ ".run()" in content or "app.run()" in content
54
+ ):
54
55
  cache_dir = directory / "cache"
55
56
  logger.info(f"Detected project root: {directory}")
56
57
  return cache_dir
57
- except (IOError, UnicodeDecodeError):
58
+ except (OSError, UnicodeDecodeError):
58
59
  continue
59
-
60
+
60
61
  # Fallback to current directory + cache
61
62
  fallback_cache = current_dir / "cache"
62
63
  logger.info(f"Project root not detected, using fallback: {fallback_cache}")
63
64
  return fallback_cache
64
-
65
+
65
66
  def ensure_cache_directories(self) -> None:
66
67
  """Create cache directory structure if it doesn't exist."""
67
68
  cache_root = self.get_cache_root()
68
69
  cache_root.mkdir(exist_ok=True)
69
-
70
+
70
71
  # Create subdirectories
71
72
  (cache_root / "users").mkdir(exist_ok=True)
72
73
  (cache_root / "tables").mkdir(exist_ok=True)
73
74
  (cache_root / "states").mkdir(exist_ok=True)
74
-
75
+
75
76
  logger.debug(f"Cache directories ensured at: {cache_root}")
76
-
77
- def get_cache_file_path(self, cache_type: str, tenant_id: str, user_id: str = None) -> Path:
77
+
78
+ def get_cache_file_path(
79
+ self, cache_type: str, tenant_id: str, user_id: str = None
80
+ ) -> Path:
78
81
  """
79
82
  Get the file path for a cache file.
80
-
83
+
81
84
  Args:
82
85
  cache_type: "users", "tables", or "states"
83
86
  tenant_id: Tenant identifier
84
87
  user_id: User identifier (required for users and states)
85
-
88
+
86
89
  Returns:
87
90
  Path to cache file
88
91
  """
89
92
  cache_root = self.get_cache_root()
90
-
93
+
91
94
  if cache_type == "users":
92
95
  if not user_id:
93
96
  raise ValueError("user_id is required for users cache")
@@ -100,39 +103,39 @@ class FileManager:
100
103
  return cache_root / "states" / f"{tenant_id}_{user_id}_state.json"
101
104
  else:
102
105
  raise ValueError(f"Invalid cache_type: {cache_type}")
103
-
104
- async def read_file(self, file_path: Path) -> Dict[str, Any]:
106
+
107
+ async def read_file(self, file_path: Path) -> dict[str, Any]:
105
108
  """Read and parse JSON file with file locking."""
106
109
  async with self._get_file_lock(str(file_path)):
107
110
  if not file_path.exists():
108
111
  return {}
109
-
112
+
110
113
  try:
111
- content = await asyncio.to_thread(file_path.read_text, encoding='utf-8')
114
+ content = await asyncio.to_thread(file_path.read_text, encoding="utf-8")
112
115
  return from_json_string(content)
113
- except (IOError, json.JSONDecodeError) as e:
116
+ except (OSError, json.JSONDecodeError) as e:
114
117
  logger.error(f"Failed to read file {file_path}: {e}")
115
118
  return {}
116
-
117
- async def write_file(self, file_path: Path, data: Dict[str, Any]) -> bool:
119
+
120
+ async def write_file(self, file_path: Path, data: dict[str, Any]) -> bool:
118
121
  """Write data to JSON file with file locking."""
119
122
  async with self._get_file_lock(str(file_path)):
120
123
  try:
121
124
  # Ensure parent directory exists
122
125
  file_path.parent.mkdir(parents=True, exist_ok=True)
123
-
126
+
124
127
  # Write to temporary file first, then rename (atomic operation)
125
- temp_file = file_path.with_suffix(file_path.suffix + '.tmp')
128
+ temp_file = file_path.with_suffix(file_path.suffix + ".tmp")
126
129
  content = to_json_string(data)
127
-
128
- await asyncio.to_thread(temp_file.write_text, content, encoding='utf-8')
130
+
131
+ await asyncio.to_thread(temp_file.write_text, content, encoding="utf-8")
129
132
  await asyncio.to_thread(temp_file.replace, file_path)
130
-
133
+
131
134
  return True
132
- except IOError as e:
135
+ except OSError as e:
133
136
  logger.error(f"Failed to write file {file_path}: {e}")
134
137
  return False
135
-
138
+
136
139
  async def delete_file(self, file_path: Path) -> bool:
137
140
  """Delete file with file locking."""
138
141
  async with self._get_file_lock(str(file_path)):
@@ -140,14 +143,14 @@ class FileManager:
140
143
  if file_path.exists():
141
144
  await asyncio.to_thread(file_path.unlink)
142
145
  return True
143
- except IOError as e:
146
+ except OSError as e:
144
147
  logger.error(f"Failed to delete file {file_path}: {e}")
145
148
  return False
146
-
149
+
147
150
  async def file_exists(self, file_path: Path) -> bool:
148
151
  """Check if file exists."""
149
152
  return await asyncio.to_thread(file_path.exists)
150
153
 
151
154
 
152
155
  # Global file manager instance
153
- file_manager = FileManager()
156
+ file_manager = FileManager()
@@ -8,4 +8,4 @@ across all cache implementations.
8
8
  from ....redis.redis_handler.utils.key_factory import KeyFactory, default_key_factory
9
9
 
10
10
  # Export the same key factory used by Redis for consistency
11
- __all__ = ["KeyFactory", "default_key_factory"]
11
+ __all__ = ["KeyFactory", "default_key_factory"]
@@ -60,30 +60,32 @@ def deserialize_from_json(data: Any, model: type[BaseModel] | None = None) -> An
60
60
  """Deserialize data from JSON storage."""
61
61
  if data is None:
62
62
  return None
63
-
63
+
64
64
  # Convert datetime strings back to datetime objects
65
65
  data = _convert_iso_strings_to_datetime(data)
66
-
66
+
67
67
  if model is not None:
68
68
  return model.model_validate(data)
69
-
69
+
70
70
  return data
71
71
 
72
72
 
73
- def create_cache_file_data(data: dict[str, Any], ttl: int | None = None) -> dict[str, Any]:
73
+ def create_cache_file_data(
74
+ data: dict[str, Any], ttl: int | None = None
75
+ ) -> dict[str, Any]:
74
76
  """Create JSON cache file structure with metadata."""
75
77
  now = datetime.now()
76
78
  expires_at = None
77
79
  if ttl:
78
80
  expires_at = datetime.fromtimestamp(now.timestamp() + ttl)
79
-
81
+
80
82
  return {
81
83
  "_metadata": {
82
84
  "created_at": now.isoformat(),
83
85
  "expires_at": expires_at.isoformat() if expires_at else None,
84
- "version": "1.0"
86
+ "version": "1.0",
85
87
  },
86
- "data": serialize_for_json(data)
88
+ "data": serialize_for_json(data),
87
89
  }
88
90
 
89
91
 
@@ -91,10 +93,10 @@ def extract_cache_file_data(file_data: dict[str, Any]) -> dict[str, Any] | None:
91
93
  """Extract data from JSON cache file, checking expiration."""
92
94
  if not isinstance(file_data, dict):
93
95
  return None
94
-
96
+
95
97
  metadata = file_data.get("_metadata", {})
96
98
  expires_at_str = metadata.get("expires_at")
97
-
99
+
98
100
  # Check expiration
99
101
  if expires_at_str:
100
102
  try:
@@ -103,7 +105,7 @@ def extract_cache_file_data(file_data: dict[str, Any]) -> dict[str, Any] | None:
103
105
  return None # Expired
104
106
  except ValueError:
105
107
  logger.warning(f"Invalid expires_at format: {expires_at_str}")
106
-
108
+
107
109
  return file_data.get("data", {})
108
110
 
109
111
 
@@ -118,4 +120,4 @@ def from_json_string(json_str: str) -> Any:
118
120
  return json.loads(json_str)
119
121
  except json.JSONDecodeError as e:
120
122
  logger.error(f"Failed to parse JSON: {e}")
121
- raise
123
+ raise
@@ -20,7 +20,7 @@ class JSONCacheFactory(ICacheFactory):
20
20
 
21
21
  Uses file-based JSON storage with proper file management:
22
22
  - State cache: Uses states subdirectory
23
- - User cache: Uses users subdirectory
23
+ - User cache: Uses users subdirectory
24
24
  - Table cache: Uses tables subdirectory
25
25
 
26
26
  All instances implement the ICache interface through adapters.
@@ -45,9 +45,7 @@ class JSONCacheFactory(ICacheFactory):
45
45
  Returns:
46
46
  ICache adapter wrapping JSONStateHandler
47
47
  """
48
- return JSONStateCacheAdapter(
49
- tenant_id=self.tenant_id, user_id=self.user_id
50
- )
48
+ return JSONStateCacheAdapter(tenant_id=self.tenant_id, user_id=self.user_id)
51
49
 
52
50
  def create_user_cache(self) -> ICache:
53
51
  """
@@ -59,9 +57,7 @@ class JSONCacheFactory(ICacheFactory):
59
57
  Returns:
60
58
  ICache adapter wrapping JSONUser
61
59
  """
62
- return JSONUserCacheAdapter(
63
- tenant_id=self.tenant_id, user_id=self.user_id
64
- )
60
+ return JSONUserCacheAdapter(tenant_id=self.tenant_id, user_id=self.user_id)
65
61
 
66
62
  def create_table_cache(self) -> ICache:
67
63
  """
@@ -73,4 +69,4 @@ class JSONCacheFactory(ICacheFactory):
73
69
  Returns:
74
70
  ICache adapter wrapping JSONTable
75
71
  """
76
- return JSONTableCacheAdapter(tenant_id=self.tenant_id)
72
+ return JSONTableCacheAdapter(tenant_id=self.tenant_id)