python-sdk-remote 0.0.155__tar.gz → 0.0.157__tar.gz

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 python-sdk-remote might be problematic. Click here for more details.

Files changed (35) hide show
  1. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/PKG-INFO +1 -1
  2. python_sdk_remote-0.0.157/README.md +36 -0
  3. python_sdk_remote-0.0.157/python_sdk_remote/src/__init__.py +140 -0
  4. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/constants_src_mini_logger_and_logger.py +6 -1
  5. python_sdk_remote-0.0.157/python_sdk_remote/src/datetime_range.py +127 -0
  6. python_sdk_remote-0.0.157/python_sdk_remote/src/exceptions.py +7 -0
  7. python_sdk_remote-0.0.157/python_sdk_remote/src/generic_crud.py +306 -0
  8. python_sdk_remote-0.0.157/python_sdk_remote/src/http_response.py +333 -0
  9. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/mini_logger.py +33 -55
  10. python_sdk_remote-0.0.157/python_sdk_remote/src/our_obfuscation.py +285 -0
  11. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/our_object.py +32 -4
  12. python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects.py +5 -0
  13. python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects_local.py +209 -0
  14. python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects_remote.py +5 -0
  15. python_sdk_remote-0.0.157/python_sdk_remote/src/our_request.py +329 -0
  16. python_sdk_remote-0.0.157/python_sdk_remote/src/our_request_old.py +316 -0
  17. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/unified_json.py +1 -1
  18. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/utilities.py +192 -45
  19. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/validate_environment.py +2 -2
  20. python_sdk_remote-0.0.157/python_sdk_remote/src/version.py +14 -0
  21. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/PKG-INFO +1 -1
  22. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/SOURCES.txt +11 -1
  23. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/setup.py +1 -1
  24. python_sdk_remote-0.0.155/README.md +0 -16
  25. python_sdk_remote-0.0.155/python_sdk_remote/src/__init__.py +0 -0
  26. python_sdk_remote-0.0.155/python_sdk_remote/src/http_response.py +0 -147
  27. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/pyproject.toml +0 -0
  28. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/constants.py +0 -0
  29. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/is_test_data.py +0 -0
  30. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/item.py +0 -0
  31. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote/src/valid_json_versions.py +0 -0
  32. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/dependency_links.txt +0 -0
  33. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/requires.txt +0 -0
  34. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/top_level.txt +0 -0
  35. {python_sdk_remote-0.0.155 → python_sdk_remote-0.0.157}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sdk-remote
3
- Version: 0.0.155
3
+ Version: 0.0.157
4
4
  Summary: PyPI Package for Circles Python SDK Local Python
5
5
  Home-page: https://github.com/circles-zone/python-sdk-remote-python-package
6
6
  Author: Circles
@@ -0,0 +1,36 @@
1
+ # Python SDK Remote Python Package
2
+
3
+ ## TODOs
4
+
5
+ ✅ ~~TODO Add Requests.post() method which wraps requests.post() and adds logging and error handling similar to OurAxios.post() in typescript-sdk-remote repo~~ - **COMPLETED**: Implemented in `src/our_request.py` with `OurRequest.post()` and `OurRequest.get()` methods
6
+
7
+ ✅ ~~TODO If we don't have DatetimeRange, develop DatetimeRange which has start_datetime and end_datetime data members~~ - **COMPLETED**: Implemented in `src/datetime_range.py` with full functionality including overlap detection, duration calculation, and serialization
8
+
9
+ TODO create OurObfuscation class with methods for each PII field obfuscation_first_name() - can have persistent mapping table in memory, obfuscation_last_name() - can have persistent mapping table in memory, obfuscation_organization_name() - can be persistent mapping table in memory, obfuscation_email_address() - can be emails in our development domain, obfuscation_organization(), obfuscation_phone_number() - preferable invalid phone numbers, obfuscation_social_id() - preferable valid social ids - obfuscation only when environment is not Prod1 and not Dvlp1.
10
+ Should do the same both in python-sdk-remote and typeScript-sdk-remote<br>
11
+
12
+ ✅ ~~TODO Step #1 create src/version.py from setup.py and pyproject.toml like we have in TypeScript `const PACKAGE_VERSION = "0.0.122"`. Step #2 Create a function get_package_version() which returns the version of the package from src/version.py which we can use in all packages.~~ - **COMPLETED**: Implemented in `src/version.py` and `get_package_version()` function in `utilities.py`
13
+
14
+ TODO Rewrite more examples
15
+
16
+ TODO Function which returns SmartHyperlink to any entity person, contact, profile, organization , event ... using Mustache format. Allow to to hyperlink multiple entities (i.e. either this person or this person). Use this function in Text Block when we identify entity. When retrieving any text, we can ask with or without SmartHyperlink (default without SmartHyperlink).
17
+
18
+ ## Permission to the repo
19
+ Many Team Members (python-team, typescript-team, reactjs-team ...) are using makec/MakePy/MakeTs to CreateDevelopmentEnvironment.ps and they need read access to this repo to build their local environment.
20
+
21
+ ## Usage in Serverless.com
22
+
23
+ In serverless, use this decorator:
24
+ ```py
25
+ from python_sdk_remote.http_response import handler_decorator
26
+ from logger_local.Logger import Logger
27
+ logger = Logger.create_logger(object=your_logger_object)
28
+ @handler_decorator(logger=logger)
29
+ def my_handler(request_parameters: dict) -> dict:
30
+ # Here you can use both camelCase and snake_case keys from request_parameters
31
+ ```
32
+
33
+ ## Versions
34
+
35
+ 0.0.151 for PRINT_STARS
36
+ 0.0.152 LogMessageSeverity and StartEndEnum values in UPPERCASE
@@ -0,0 +1,140 @@
1
+ """
2
+ Python SDK Remote - Main exports.
3
+
4
+ This module exports all the main classes and functions from the python-sdk-remote package.
5
+ """
6
+
7
+ # Core utilities
8
+ from .utilities import (
9
+ get_package_version,
10
+ get_environment_name,
11
+ get_brand_name,
12
+ our_get_env,
13
+ EnvironmentVariableException,
14
+ our_set_env,
15
+ our_delete_env,
16
+ to_dict,
17
+ to_json,
18
+ snake_to_camel,
19
+ camel_to_snake,
20
+ obfuscate_log_dict,
21
+ validate_url,
22
+ timedelta_to_time_format,
23
+ is_list_of_dicts,
24
+ is_valid_time_range,
25
+ is_valid_date_range,
26
+ is_valid_datetime_range,
27
+ is_time_in_time_range,
28
+ is_date_in_date_range,
29
+ is_datetime_in_datetime_range,
30
+ encode_jwt,
31
+ decode_jwt,
32
+ generate_otp,
33
+ get_current_datetime_string,
34
+ datetime_from_str,
35
+ datetime_to_str,
36
+ validate_arguments,
37
+ reformat_time_string,
38
+ get_ip_v4,
39
+ get_ip_v6,
40
+ deprecation_warning_function,
41
+ )
42
+
43
+ # HTTP response utilities
44
+ from .http_response import (
45
+ get_body_payload_dict_from_event,
46
+ get_payload_dict_from_event,
47
+ get_path_parameters_dict_from_event,
48
+ get_query_string_parameters_from_event,
49
+ get_request_parameters_from_event,
50
+ handler_decorator,
51
+ create_authorization_http_headers,
52
+ get_user_jwt_from_event,
53
+ create_return_http_headers,
54
+ create_error_http_response,
55
+ create_ok_http_response,
56
+ create_http_body,
57
+ create_internal_server_error_http_response,
58
+ create_ok_http_response_simple,
59
+ )
60
+
61
+ # New classes implemented for TODOs
62
+ from .datetime_range import DatetimeRange
63
+ from .our_obfuscation import OurObfuscation
64
+ from .our_request import OurRequest
65
+ from .generic_crud import GenericCrud, MockGenericCrud
66
+ from .our_objects_local import OurObjectsLocal
67
+
68
+ # Existing classes
69
+ from .mini_logger import MiniLogger
70
+ from .our_object import OurObject
71
+ from .version import PACKAGE_VERSION
72
+
73
+ # Constants
74
+ # Note: Importing constants separately to avoid circular imports
75
+ # from .constants import *
76
+
77
+ __all__ = [
78
+ # Utilities
79
+ 'get_package_version',
80
+ 'get_environment_name',
81
+ 'get_brand_name',
82
+ 'our_get_env',
83
+ 'EnvironmentVariableException',
84
+ 'our_set_env',
85
+ 'our_delete_env',
86
+ 'to_dict',
87
+ 'to_json',
88
+ 'snake_to_camel',
89
+ 'camel_to_snake',
90
+ 'obfuscate_log_dict',
91
+ 'validate_url',
92
+ 'timedelta_to_time_format',
93
+ 'is_list_of_dicts',
94
+ 'is_valid_time_range',
95
+ 'is_valid_date_range',
96
+ 'is_valid_datetime_range',
97
+ 'is_time_in_time_range',
98
+ 'is_date_in_date_range',
99
+ 'is_datetime_in_datetime_range',
100
+ 'encode_jwt',
101
+ 'decode_jwt',
102
+ 'generate_otp',
103
+ 'get_current_datetime_string',
104
+ 'datetime_from_str',
105
+ 'datetime_to_str',
106
+ 'validate_arguments',
107
+ 'reformat_time_string',
108
+ 'get_ip_v4',
109
+ 'get_ip_v6',
110
+ 'deprecation_warning_function',
111
+
112
+ # HTTP response utilities
113
+ 'get_body_payload_dict_from_event',
114
+ 'get_payload_dict_from_event',
115
+ 'get_path_parameters_dict_from_event',
116
+ 'get_query_string_parameters_from_event',
117
+ 'get_request_parameters_from_event',
118
+ 'handler_decorator',
119
+ 'create_authorization_http_headers',
120
+ 'get_user_jwt_from_event',
121
+ 'create_return_http_headers',
122
+ 'create_error_http_response',
123
+ 'create_ok_http_response',
124
+ 'create_http_body',
125
+ 'create_internal_server_error_http_response',
126
+ 'create_ok_http_response_simple',
127
+
128
+ # New classes
129
+ 'DatetimeRange',
130
+ 'OurObfuscation',
131
+ 'OurRequest',
132
+ 'GenericCrud',
133
+ 'MockGenericCrud',
134
+ 'OurObjectsLocal',
135
+
136
+ # Existing classes
137
+ 'MiniLogger',
138
+ 'OurObject',
139
+ 'PACKAGE_VERSION',
140
+ ]
@@ -1,8 +1,12 @@
1
1
  from enum import Enum
2
2
 
3
3
 
4
- # TODO I think enum values should be in uppercase
5
4
  class LogMessageSeverity(Enum):
5
+ """
6
+ Log message severity levels.
7
+
8
+ Note: Enum values are in uppercase following Python conventions.
9
+ """
6
10
  DEBUG = 100
7
11
  VERBOSE = 200
8
12
  INIT = 300
@@ -14,5 +18,6 @@ class LogMessageSeverity(Enum):
14
18
 
15
19
 
16
20
  class StartEndEnum(Enum):
21
+ """Enum for marking start and end of operations."""
17
22
  START = 400
18
23
  END = 402
@@ -0,0 +1,127 @@
1
+ """
2
+ DatetimeRange class for handling datetime ranges.
3
+
4
+ This module provides a DatetimeRange class with start_datetime and end_datetime data members
5
+ as requested in the TODO.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ from .mini_logger import MiniLogger as logger
12
+
13
+
14
+ class DatetimeRange:
15
+ """
16
+ A class to represent a datetime range with start and end datetime.
17
+
18
+ Attributes:
19
+ start_datetime (datetime): The start datetime of the range
20
+ end_datetime (datetime): The end datetime of the range
21
+ """
22
+
23
+ def __init__(self, start_datetime: datetime, end_datetime: datetime):
24
+ """
25
+ Initialize DatetimeRange with start and end datetime.
26
+
27
+ Args:
28
+ start_datetime: The start datetime of the range
29
+ end_datetime: The end datetime of the range
30
+
31
+ Raises:
32
+ ValueError: If start_datetime is after end_datetime
33
+ """
34
+ logger.start(object={"start_datetime": start_datetime, "end_datetime": end_datetime})
35
+
36
+ if start_datetime > end_datetime:
37
+ raise ValueError("start_datetime cannot be after end_datetime")
38
+
39
+ self.start_datetime = start_datetime
40
+ self.end_datetime = end_datetime
41
+
42
+ logger.end(object={"datetime_range": self.__dict__})
43
+
44
+ def contains(self, check_datetime: datetime) -> bool:
45
+ """
46
+ Check if a datetime is within this range (inclusive).
47
+
48
+ Args:
49
+ check_datetime: The datetime to check
50
+
51
+ Returns:
52
+ bool: True if the datetime is within the range, False otherwise
53
+ """
54
+ logger.start(object={"check_datetime": check_datetime})
55
+ result = self.start_datetime <= check_datetime <= self.end_datetime
56
+ logger.end(object={"contains_result": result})
57
+ return result
58
+
59
+ def overlaps(self, other: 'DatetimeRange') -> bool:
60
+ """
61
+ Check if this range overlaps with another DatetimeRange.
62
+
63
+ Args:
64
+ other: Another DatetimeRange to check overlap with
65
+
66
+ Returns:
67
+ bool: True if ranges overlap, False otherwise
68
+ """
69
+ logger.start(object={"other_range": other.__dict__})
70
+ result = (self.start_datetime <= other.end_datetime and
71
+ self.end_datetime >= other.start_datetime)
72
+ logger.end(object={"overlaps_result": result})
73
+ return result
74
+
75
+ def duration(self) -> datetime:
76
+ """
77
+ Calculate the duration of the datetime range.
78
+
79
+ Returns:
80
+ timedelta: The duration between start and end datetime
81
+ """
82
+ logger.start()
83
+ duration = self.end_datetime - self.start_datetime
84
+ logger.end(object={"duration": str(duration)})
85
+ return duration
86
+
87
+ def __str__(self) -> str:
88
+ """String representation of the DatetimeRange."""
89
+ return f"DatetimeRange({self.start_datetime} to {self.end_datetime})"
90
+
91
+ def __repr__(self) -> str:
92
+ """Detailed string representation of the DatetimeRange."""
93
+ return f"DatetimeRange(start_datetime={self.start_datetime!r}, end_datetime={self.end_datetime!r})"
94
+
95
+ def __eq__(self, other) -> bool:
96
+ """Check equality with another DatetimeRange."""
97
+ if not isinstance(other, DatetimeRange):
98
+ return False
99
+ return (self.start_datetime == other.start_datetime and
100
+ self.end_datetime == other.end_datetime)
101
+
102
+ def to_dict(self) -> dict:
103
+ """
104
+ Convert DatetimeRange to dictionary.
105
+
106
+ Returns:
107
+ dict: Dictionary representation with start_datetime and end_datetime
108
+ """
109
+ return {
110
+ "start_datetime": self.start_datetime.isoformat(),
111
+ "end_datetime": self.end_datetime.isoformat()
112
+ }
113
+
114
+ @classmethod
115
+ def from_dict(cls, data: dict) -> 'DatetimeRange':
116
+ """
117
+ Create DatetimeRange from dictionary.
118
+
119
+ Args:
120
+ data: Dictionary with start_datetime and end_datetime keys
121
+
122
+ Returns:
123
+ DatetimeRange: New DatetimeRange instance
124
+ """
125
+ start_dt = datetime.fromisoformat(data["start_datetime"])
126
+ end_dt = datetime.fromisoformat(data["end_datetime"])
127
+ return cls(start_dt, end_dt)
@@ -0,0 +1,7 @@
1
+ # TODO Rename the file to environment_variable_exception.py
2
+ class EnvironmentVariableException(Exception):
3
+ """Raised when a required environment variable is missing or empty.
4
+
5
+ A specific exception (rather than a generic Exception) so callers can catch
6
+ exactly this case; shared by our_get_env / our_delete_env for consistency.
7
+ """
@@ -0,0 +1,306 @@
1
+ """
2
+ GenericCrud class for consistent database operations.
3
+
4
+ This module provides a generic CRUD interface that can be used by OurObjectsLocal
5
+ and other database access classes for consistent operations.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from .mini_logger import MiniLogger as logger
12
+
13
+
14
+ class GenericCrud(ABC):
15
+ """
16
+ Abstract base class for generic CRUD operations.
17
+
18
+ Provides a consistent interface for database operations that can be
19
+ implemented by different database backends (MySQL, PostgreSQL, etc.).
20
+ """
21
+
22
+ def __init__(self, table_name: str, connection=None):
23
+ """
24
+ Initialize GenericCrud with table name and optional connection.
25
+
26
+ Args:
27
+ table_name: Name of the database table
28
+ connection: Database connection object (optional)
29
+ """
30
+ self.table_name = table_name
31
+ self.connection = connection
32
+ logger.info(f"GenericCrud initialized for table_name={table_name}")
33
+
34
+ @abstractmethod
35
+ def create(self, data: Dict[str, Any]) -> Optional[int]:
36
+ """
37
+ Create a new record in the database.
38
+
39
+ Args:
40
+ data: Dictionary of field names and values
41
+
42
+ Returns:
43
+ Optional[int]: ID of the created record, or None if creation failed
44
+ """
45
+ pass
46
+
47
+ @abstractmethod
48
+ def read(self, record_id: Union[int, str]) -> Optional[Dict[str, Any]]:
49
+ """
50
+ Read a single record by ID.
51
+
52
+ Args:
53
+ record_id: ID of the record to read
54
+
55
+ Returns:
56
+ Optional[Dict]: Record data as dictionary, or None if not found
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def update(self, record_id: Union[int, str], data: Dict[str, Any]) -> bool:
62
+ """
63
+ Update an existing record.
64
+
65
+ Args:
66
+ record_id: ID of the record to update
67
+ data: Dictionary of field names and new values
68
+
69
+ Returns:
70
+ bool: True if update was successful, False otherwise
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def delete(self, record_id: Union[int, str]) -> bool:
76
+ """
77
+ Delete a record by ID.
78
+
79
+ Args:
80
+ record_id: ID of the record to delete
81
+
82
+ Returns:
83
+ bool: True if deletion was successful, False otherwise
84
+ """
85
+ pass
86
+
87
+ @abstractmethod
88
+ def list_all(self, limit: Optional[int] = None, offset: int = 0) -> List[Dict[str, Any]]:
89
+ """
90
+ List all records in the table.
91
+
92
+ Args:
93
+ limit: Maximum number of records to return (optional)
94
+ offset: Number of records to skip (default: 0)
95
+
96
+ Returns:
97
+ List[Dict]: List of records as dictionaries
98
+ """
99
+ pass
100
+
101
+ @abstractmethod
102
+ def where(self, conditions: Dict[str, Any],
103
+ limit: Optional[int] = None,
104
+ offset: int = 0,
105
+ order_by: Optional[str] = None) -> List[Dict[str, Any]]:
106
+ """
107
+ Query records with WHERE conditions.
108
+
109
+ Args:
110
+ conditions: Dictionary of field names and values for WHERE clause
111
+ limit: Maximum number of records to return (optional)
112
+ offset: Number of records to skip (default: 0)
113
+ order_by: Field name to order by (optional)
114
+
115
+ Returns:
116
+ List[Dict]: List of matching records as dictionaries
117
+ """
118
+ pass
119
+
120
+ @abstractmethod
121
+ def count(self, conditions: Optional[Dict[str, Any]] = None) -> int:
122
+ """
123
+ Count records matching conditions.
124
+
125
+ Args:
126
+ conditions: Dictionary of field names and values for WHERE clause (optional)
127
+
128
+ Returns:
129
+ int: Number of matching records
130
+ """
131
+ pass
132
+
133
+ def active_by_filter(self, filter_criteria: Dict[str, Any]) -> List[Dict[str, Any]]:
134
+ """
135
+ Get active records matching filter criteria.
136
+
137
+ This is a convenience method that adds 'active' = True to the filter criteria.
138
+
139
+ Args:
140
+ filter_criteria: Dictionary of field names and values
141
+
142
+ Returns:
143
+ List[Dict]: List of active records matching criteria
144
+ """
145
+ logger.start(object={"filter_criteria": filter_criteria})
146
+
147
+ # Add active condition to filter criteria
148
+ active_filter = filter_criteria.copy()
149
+ active_filter["active"] = True
150
+
151
+ result = self.where(active_filter)
152
+
153
+ logger.end(object={"result_count": len(result)})
154
+ return result
155
+
156
+ def active(self) -> List[Dict[str, Any]]:
157
+ """
158
+ Get all active records.
159
+
160
+ Returns:
161
+ List[Dict]: List of all active records
162
+ """
163
+ logger.start()
164
+ result = self.active_by_filter({})
165
+ logger.end(object={"result_count": len(result)})
166
+ return result
167
+
168
+
169
+ class MockGenericCrud(GenericCrud):
170
+ """
171
+ Mock implementation of GenericCrud for testing purposes.
172
+
173
+ Uses in-memory storage instead of a real database.
174
+ """
175
+
176
+ def __init__(self, table_name: str, connection=None):
177
+ super().__init__(table_name, connection)
178
+ self._data: Dict[int, Dict[str, Any]] = {}
179
+ self._next_id = 1
180
+
181
+ def create(self, data: Dict[str, Any]) -> Optional[int]:
182
+ """Create a new record in memory."""
183
+ logger.start(object={"data": data})
184
+
185
+ record_id = self._next_id
186
+ self._next_id += 1
187
+
188
+ # Add ID and created timestamp
189
+ record_data = data.copy()
190
+ record_data["id"] = record_id
191
+
192
+ self._data[record_id] = record_data
193
+
194
+ logger.end(object={"record_id": record_id})
195
+ return record_id
196
+
197
+ def read(self, record_id: Union[int, str]) -> Optional[Dict[str, Any]]:
198
+ """Read a single record by ID."""
199
+ logger.start(object={"record_id": record_id})
200
+
201
+ record_id = int(record_id)
202
+ result = self._data.get(record_id)
203
+
204
+ logger.end(object={"found": result is not None})
205
+ return result
206
+
207
+ def update(self, record_id: Union[int, str], data: Dict[str, Any]) -> bool:
208
+ """Update an existing record."""
209
+ logger.start(object={"record_id": record_id, "data": data})
210
+
211
+ record_id = int(record_id)
212
+ if record_id in self._data:
213
+ self._data[record_id].update(data)
214
+ logger.end(object={"success": True})
215
+ return True
216
+
217
+ logger.end(object={"success": False, "reason": "record_not_found"})
218
+ return False
219
+
220
+ def delete(self, record_id: Union[int, str]) -> bool:
221
+ """Delete a record by ID."""
222
+ logger.start(object={"record_id": record_id})
223
+
224
+ record_id = int(record_id)
225
+ if record_id in self._data:
226
+ del self._data[record_id]
227
+ logger.end(object={"success": True})
228
+ return True
229
+
230
+ logger.end(object={"success": False, "reason": "record_not_found"})
231
+ return False
232
+
233
+ def list_all(self, limit: Optional[int] = None, offset: int = 0) -> List[Dict[str, Any]]:
234
+ """List all records."""
235
+ logger.start(object={"limit": limit, "offset": offset})
236
+
237
+ all_records = list(self._data.values())
238
+
239
+ # Apply offset and limit
240
+ if offset > 0:
241
+ all_records = all_records[offset:]
242
+
243
+ if limit is not None:
244
+ all_records = all_records[:limit]
245
+
246
+ logger.end(object={"result_count": len(all_records)})
247
+ return all_records
248
+
249
+ def where(self, conditions: Dict[str, Any],
250
+ limit: Optional[int] = None,
251
+ offset: int = 0,
252
+ order_by: Optional[str] = None) -> List[Dict[str, Any]]:
253
+ """Query records with WHERE conditions."""
254
+ logger.start(object={"conditions": conditions, "limit": limit, "offset": offset, "order_by": order_by})
255
+
256
+ matching_records = []
257
+
258
+ for record in self._data.values():
259
+ matches = True
260
+ for field, value in conditions.items():
261
+ if field not in record or record[field] != value:
262
+ matches = False
263
+ break
264
+
265
+ if matches:
266
+ matching_records.append(record)
267
+
268
+ # Apply ordering
269
+ if order_by and matching_records:
270
+ try:
271
+ matching_records.sort(key=lambda x: x.get(order_by, ''))
272
+ except Exception as e:
273
+ logger.warning(f"Failed to sort by order_by={order_by}: e={e}")
274
+
275
+ # Apply offset and limit
276
+ if offset > 0:
277
+ matching_records = matching_records[offset:]
278
+
279
+ if limit is not None:
280
+ matching_records = matching_records[:limit]
281
+
282
+ logger.end(object={"result_count": len(matching_records)})
283
+ return matching_records
284
+
285
+ def count(self, conditions: Optional[Dict[str, Any]] = None) -> int:
286
+ """Count records matching conditions."""
287
+ logger.start(object={"conditions": conditions})
288
+
289
+ if conditions is None:
290
+ result = len(self._data)
291
+ else:
292
+ matching_count = 0
293
+ for record in self._data.values():
294
+ matches = True
295
+ for field, value in conditions.items():
296
+ if field not in record or record[field] != value:
297
+ matches = False
298
+ break
299
+
300
+ if matches:
301
+ matching_count += 1
302
+
303
+ result = matching_count
304
+
305
+ logger.end(object={"count": result})
306
+ return result