python-sdk-remote 0.0.156__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.
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/PKG-INFO +1 -1
- python_sdk_remote-0.0.157/README.md +36 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/__init__.py +140 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/constants_src_mini_logger_and_logger.py +6 -1
- python_sdk_remote-0.0.157/python_sdk_remote/src/datetime_range.py +127 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/exceptions.py +7 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/generic_crud.py +306 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/http_response.py +333 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/mini_logger.py +33 -55
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_obfuscation.py +285 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/our_object.py +32 -4
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects.py +5 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects_local.py +209 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_objects_remote.py +5 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_request.py +329 -0
- python_sdk_remote-0.0.157/python_sdk_remote/src/our_request_old.py +316 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/unified_json.py +1 -1
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/utilities.py +192 -45
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/validate_environment.py +2 -2
- python_sdk_remote-0.0.157/python_sdk_remote/src/version.py +14 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/PKG-INFO +1 -1
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/SOURCES.txt +8 -1
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/setup.py +1 -1
- python_sdk_remote-0.0.156/README.md +0 -32
- python_sdk_remote-0.0.156/python_sdk_remote/src/__init__.py +0 -0
- python_sdk_remote-0.0.156/python_sdk_remote/src/http_response.py +0 -147
- python_sdk_remote-0.0.156/python_sdk_remote/src/our_objects.py +0 -5
- python_sdk_remote-0.0.156/python_sdk_remote/src/our_objects_local.py +0 -12
- python_sdk_remote-0.0.156/python_sdk_remote/src/our_objects_remote.py +0 -5
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/pyproject.toml +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/constants.py +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/is_test_data.py +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/item.py +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote/src/valid_json_versions.py +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/dependency_links.txt +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/requires.txt +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/python_sdk_remote.egg-info/top_level.txt +0 -0
- {python_sdk_remote-0.0.156 → python_sdk_remote-0.0.157}/setup.cfg +0 -0
|
@@ -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
|