laibon 0.0.12__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.
- laibon-0.0.12/.gitignore +4 -0
- laibon-0.0.12/LICENSE +19 -0
- laibon-0.0.12/PKG-INFO +89 -0
- laibon-0.0.12/README.md +75 -0
- laibon-0.0.12/pyproject.toml +28 -0
- laibon-0.0.12/src/laibon/__init__.py +0 -0
- laibon-0.0.12/src/laibon/accessor.py +18 -0
- laibon-0.0.12/src/laibon/activity.py +102 -0
- laibon-0.0.12/src/laibon/common.py +189 -0
- laibon-0.0.12/src/laibon/db.py +191 -0
- laibon-0.0.12/src/laibon/exception.py +57 -0
- laibon-0.0.12/src/laibon/rest.py +295 -0
- laibon-0.0.12/src/laibon/rtf.py +169 -0
- laibon-0.0.12/src/requirements.txt +3 -0
- laibon-0.0.12/tests/__init__.py +0 -0
- laibon-0.0.12/tests/test_accessor.py +29 -0
- laibon-0.0.12/tests/test_caching.py +58 -0
- laibon-0.0.12/tests/test_container.py +29 -0
- laibon-0.0.12/tests/test_db.py +67 -0
- laibon-0.0.12/tests/test_exception.py +36 -0
- laibon-0.0.12/tests/test_flow_mgt.py +140 -0
- laibon-0.0.12/tests/test_json_validation.py +72 -0
- laibon-0.0.12/tests/test_rest.py +6 -0
laibon-0.0.12/.gitignore
ADDED
laibon-0.0.12/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2023 Wenceslaus Mumala
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
laibon-0.0.12/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: laibon
|
|
3
|
+
Version: 0.0.12
|
|
4
|
+
Summary: Common library for django development
|
|
5
|
+
Author-email: Wenceslaus Mumala <info@dvectors.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: django>=4.2
|
|
12
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Laibon
|
|
16
|
+
|
|
17
|
+
Laibon is a collection of useful common classes that can go a long way to help in developing a django based web application.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Database Access**: Abstract adapter pattern for clean separation between business logic and Django models
|
|
22
|
+
- **Flow Management**: Activity-based workflow execution with conditional jumps and error handling
|
|
23
|
+
- **Container Pattern**: Thread-safe key-value storage for passing data between activities
|
|
24
|
+
- **JSON Validation**: Schema-based request validation with caching for performance
|
|
25
|
+
- **Exception Handling**: Comprehensive exception hierarchy for different error scenarios
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
From PyPI:
|
|
30
|
+
```bash
|
|
31
|
+
python3 -m pip install laibon
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
From TestPyPI:
|
|
35
|
+
```bash
|
|
36
|
+
pip install --extra-index-url https://testpypi.python.org/pypi laibon
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Examples
|
|
40
|
+
|
|
41
|
+
### Database Adapter Pattern
|
|
42
|
+
```python
|
|
43
|
+
from laibon.db import Adapter, BaseModel
|
|
44
|
+
|
|
45
|
+
class UserAdapter(Adapter):
|
|
46
|
+
def __init__(self, entity_id=None, name=None, email=None):
|
|
47
|
+
super().__init__(entity_id)
|
|
48
|
+
self.name = name
|
|
49
|
+
self.email = email
|
|
50
|
+
|
|
51
|
+
def to_model(self, existing=None):
|
|
52
|
+
if existing:
|
|
53
|
+
existing.name = self.name
|
|
54
|
+
return existing
|
|
55
|
+
return UserModel(name=self.name, email=self.email)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Flow Management
|
|
59
|
+
```python
|
|
60
|
+
from laibon.rtf import FlowDefinition, FlowRunner
|
|
61
|
+
|
|
62
|
+
flow = FlowDefinition("User Registration")
|
|
63
|
+
flow.add(ValidateInputActivity) \
|
|
64
|
+
.jump_if(ValidationResult.INVALID, ErrorActivity) \
|
|
65
|
+
.jump_default(CreateUserActivity)
|
|
66
|
+
|
|
67
|
+
runner = FlowRunner(data_container)
|
|
68
|
+
runner.run_flow(flow)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### JSON Validation
|
|
72
|
+
```python
|
|
73
|
+
from laibon.rest import JSONSchemaValidator
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
JSONSchemaValidator.validate_schema(
|
|
77
|
+
{"name": "John", "age": 30},
|
|
78
|
+
"user/create_request.json"
|
|
79
|
+
)
|
|
80
|
+
except JSONValidationException:
|
|
81
|
+
# Handle validation error
|
|
82
|
+
pass
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
|
|
87
|
+
- Python >= 3.8
|
|
88
|
+
- Django >= 4.2
|
|
89
|
+
- jsonschema >= 4.0.0
|
laibon-0.0.12/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Laibon
|
|
2
|
+
|
|
3
|
+
Laibon is a collection of useful common classes that can go a long way to help in developing a django based web application.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Database Access**: Abstract adapter pattern for clean separation between business logic and Django models
|
|
8
|
+
- **Flow Management**: Activity-based workflow execution with conditional jumps and error handling
|
|
9
|
+
- **Container Pattern**: Thread-safe key-value storage for passing data between activities
|
|
10
|
+
- **JSON Validation**: Schema-based request validation with caching for performance
|
|
11
|
+
- **Exception Handling**: Comprehensive exception hierarchy for different error scenarios
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
From PyPI:
|
|
16
|
+
```bash
|
|
17
|
+
python3 -m pip install laibon
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
From TestPyPI:
|
|
21
|
+
```bash
|
|
22
|
+
pip install --extra-index-url https://testpypi.python.org/pypi laibon
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Examples
|
|
26
|
+
|
|
27
|
+
### Database Adapter Pattern
|
|
28
|
+
```python
|
|
29
|
+
from laibon.db import Adapter, BaseModel
|
|
30
|
+
|
|
31
|
+
class UserAdapter(Adapter):
|
|
32
|
+
def __init__(self, entity_id=None, name=None, email=None):
|
|
33
|
+
super().__init__(entity_id)
|
|
34
|
+
self.name = name
|
|
35
|
+
self.email = email
|
|
36
|
+
|
|
37
|
+
def to_model(self, existing=None):
|
|
38
|
+
if existing:
|
|
39
|
+
existing.name = self.name
|
|
40
|
+
return existing
|
|
41
|
+
return UserModel(name=self.name, email=self.email)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Flow Management
|
|
45
|
+
```python
|
|
46
|
+
from laibon.rtf import FlowDefinition, FlowRunner
|
|
47
|
+
|
|
48
|
+
flow = FlowDefinition("User Registration")
|
|
49
|
+
flow.add(ValidateInputActivity) \
|
|
50
|
+
.jump_if(ValidationResult.INVALID, ErrorActivity) \
|
|
51
|
+
.jump_default(CreateUserActivity)
|
|
52
|
+
|
|
53
|
+
runner = FlowRunner(data_container)
|
|
54
|
+
runner.run_flow(flow)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### JSON Validation
|
|
58
|
+
```python
|
|
59
|
+
from laibon.rest import JSONSchemaValidator
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
JSONSchemaValidator.validate_schema(
|
|
63
|
+
{"name": "John", "age": 30},
|
|
64
|
+
"user/create_request.json"
|
|
65
|
+
)
|
|
66
|
+
except JSONValidationException:
|
|
67
|
+
# Handle validation error
|
|
68
|
+
pass
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Python >= 3.8
|
|
74
|
+
- Django >= 4.2
|
|
75
|
+
- jsonschema >= 4.0.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "laibon"
|
|
3
|
+
dependencies = [
|
|
4
|
+
"django>=4.2",
|
|
5
|
+
"jsonschema>=4.0.0"
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
version = "0.0.12"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Wenceslaus Mumala", email = "info@dvectors.com" },
|
|
11
|
+
]
|
|
12
|
+
description = "Common library for django development"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
#"Homepage" = "http://"
|
|
24
|
+
#"Bug Tracker" = "http://"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright Wenceslaus Mumala 2023. See LICENSE file.
|
|
2
|
+
|
|
3
|
+
from laibon import common
|
|
4
|
+
from laibon import exception
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DelayedExceptionAccessor:
|
|
8
|
+
_KEY = common.ContainerKey("DelayedException")
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def set(data, data_container: common.Container):
|
|
12
|
+
if data is None:
|
|
13
|
+
raise exception.InvalidArgumentException("Cannot set a null object")
|
|
14
|
+
data_container.put(DelayedExceptionAccessor._KEY, data)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get(data_container: common.Container):
|
|
18
|
+
return data_container.get(DelayedExceptionAccessor._KEY)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright (c) 2023 Wenceslaus Mumala
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
|
|
7
|
+
from laibon import accessor
|
|
8
|
+
from laibon import common
|
|
9
|
+
from laibon import db
|
|
10
|
+
from laibon import exception
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PersistentDataAccessor:
|
|
14
|
+
_KEY = common.ContainerKey("PersistentData")
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def set(data, data_container: common.Container):
|
|
18
|
+
raise exception.InvalidArgumentException("Please use PersistentData#add")
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def get(data_container: common.Container) -> db.PersistentData:
|
|
22
|
+
res = data_container.get(PersistentDataAccessor._KEY)
|
|
23
|
+
if res is None:
|
|
24
|
+
res = db.PersistentData()
|
|
25
|
+
data_container.put(PersistentDataAccessor._KEY, res)
|
|
26
|
+
return res
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WriteDataActivity(common.Activity):
|
|
30
|
+
LOGGER = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
class Result(common.ActivityResult):
|
|
36
|
+
SUCCESS = "Success"
|
|
37
|
+
WRITE_ERROR = "Failed"
|
|
38
|
+
|
|
39
|
+
def get_result_codes(self):
|
|
40
|
+
return [self.Result.SUCCESS, self.Result.WRITE_ERROR]
|
|
41
|
+
|
|
42
|
+
def create_entity(self, entity_adapter: db.Adapter):
|
|
43
|
+
try:
|
|
44
|
+
entity = entity_adapter.to_model()
|
|
45
|
+
entity.save()
|
|
46
|
+
return entity
|
|
47
|
+
except Exception as e:
|
|
48
|
+
self.LOGGER.debug("Failed to store new data because of ", exc_info=True)
|
|
49
|
+
raise exception.PersistenceException(cause=e)
|
|
50
|
+
|
|
51
|
+
def update_entity(self, delta: db.Adapter):
|
|
52
|
+
try:
|
|
53
|
+
existing = delta.get_model_class().objects.get(id=delta.id)
|
|
54
|
+
delta.to_model(existing).save()
|
|
55
|
+
except Exception as e:
|
|
56
|
+
self.LOGGER.debug("Failed to update data because of ", exc_info=True)
|
|
57
|
+
raise exception.PersistenceException(cause=e)
|
|
58
|
+
|
|
59
|
+
def delete_entity(self, delta: db.Adapter):
|
|
60
|
+
# Should be discouraged - also prone to error
|
|
61
|
+
try:
|
|
62
|
+
existing = delta.get_model_class().objects.get(id=delta.id)
|
|
63
|
+
return existing.delete()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.LOGGER.debug("Error removing data because of ", exc_info=True)
|
|
66
|
+
raise exception.PersistenceException(cause=e)
|
|
67
|
+
|
|
68
|
+
def get_changes(self, data_container: common.Container) -> db.PersistentData:
|
|
69
|
+
changes: db.PersistentData = PersistentDataAccessor.get(data_container)
|
|
70
|
+
return changes
|
|
71
|
+
|
|
72
|
+
def process(self, data_container: common.Container):
|
|
73
|
+
WriteDataActivity.LOGGER.debug("Writing flow data to database")
|
|
74
|
+
try:
|
|
75
|
+
changes = self.get_changes(data_container)
|
|
76
|
+
if changes.get_size() > 0:
|
|
77
|
+
self._write_changes(changes)
|
|
78
|
+
else:
|
|
79
|
+
WriteDataActivity.LOGGER.debug("No changes to write")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
accessor.DelayedExceptionAccessor.set(e, data_container)
|
|
82
|
+
WriteDataActivity.LOGGER.debug("Failed to write data because of ", exc_info=True)
|
|
83
|
+
return self.Result.WRITE_ERROR
|
|
84
|
+
return common.goto_next()
|
|
85
|
+
|
|
86
|
+
@transaction.atomic
|
|
87
|
+
def _write_changes(self, changes: db.PersistentData):
|
|
88
|
+
for change in changes.iterator():
|
|
89
|
+
try:
|
|
90
|
+
if not change.is_valid():
|
|
91
|
+
continue
|
|
92
|
+
change_type: db.PersistentChange.ChangeType = change.get_change_type()
|
|
93
|
+
if db.PersistentChange.ChangeType.CREATE == change_type:
|
|
94
|
+
self.create_entity(change.get_data())
|
|
95
|
+
elif db.PersistentChange.ChangeType.UPDATE == change_type:
|
|
96
|
+
self.update_entity(change.get_data())
|
|
97
|
+
elif db.PersistentChange.ChangeType.DELETE == change_type:
|
|
98
|
+
self.delete_entity(change.get_data())
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self.LOGGER.debug("Failed to write {}".format(change.__class__))
|
|
101
|
+
raise e
|
|
102
|
+
WriteDataActivity.LOGGER.debug("Done writing data")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Copyright Wenceslaus Mumala 2023. See LICENSE file.
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import enum
|
|
5
|
+
import json
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
|
|
8
|
+
from laibon import exception
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractEnum(enum.Enum):
|
|
12
|
+
"""Base class for enums with value conversion capabilities."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, value):
|
|
15
|
+
self._value = value
|
|
16
|
+
|
|
17
|
+
def to_value(self):
|
|
18
|
+
"""Return the underlying value of this enum."""
|
|
19
|
+
return self._value
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_value(cls, val):
|
|
23
|
+
"""Create enum instance from value, returns None if not found."""
|
|
24
|
+
for v in cls:
|
|
25
|
+
if v.to_value() == val:
|
|
26
|
+
return v
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContainerKey:
|
|
31
|
+
"""Hashable key for Container storage with string-based equality."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, key):
|
|
34
|
+
self.key = key
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other):
|
|
37
|
+
return isinstance(other, ContainerKey) and self.key == other.key
|
|
38
|
+
|
|
39
|
+
def __hash__(self):
|
|
40
|
+
return hash(self.key)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Container:
|
|
44
|
+
"""Generic key-value storage for passing data between activities.
|
|
45
|
+
|
|
46
|
+
Used throughout flows to store and retrieve data. Accessors provide
|
|
47
|
+
type-safe access patterns for specific data types.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
self._container = {}
|
|
52
|
+
|
|
53
|
+
def get(self, key):
|
|
54
|
+
"""Retrieve value by key, returns None if not found."""
|
|
55
|
+
return self._container.get(key)
|
|
56
|
+
|
|
57
|
+
def put(self, key, value):
|
|
58
|
+
"""Store value with given key."""
|
|
59
|
+
self._container[key] = value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FlowRequest:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ContainerDataAccessor:
|
|
67
|
+
def __init__(self, key: ContainerKey):
|
|
68
|
+
self._key = key
|
|
69
|
+
|
|
70
|
+
def set(self, data, data_container: Container):
|
|
71
|
+
if not isinstance(data, self.get_data_type()):
|
|
72
|
+
raise exception.InvalidArgumentException()
|
|
73
|
+
data_container.put(self._key, data)
|
|
74
|
+
|
|
75
|
+
def get(self, data_container: Container):
|
|
76
|
+
return data_container.get(self._key)
|
|
77
|
+
|
|
78
|
+
def get_or_raise(self, data_container: Container):
|
|
79
|
+
v = data_container.get(self._key)
|
|
80
|
+
if v is None:
|
|
81
|
+
raise exception.MissingContainerValueException()
|
|
82
|
+
return v
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def get_data_type(self) -> type:
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ActivityResult(enum.Enum):
|
|
90
|
+
def __init__(self, value):
|
|
91
|
+
self._value = value
|
|
92
|
+
|
|
93
|
+
def to_value(self):
|
|
94
|
+
return self._value
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_value(cls, val):
|
|
98
|
+
for v in cls:
|
|
99
|
+
if v.to_value() == val:
|
|
100
|
+
return v
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DefaultActivityResult(ActivityResult):
|
|
105
|
+
# Reserved result codes
|
|
106
|
+
NEXT = "next"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def goto_next() -> ActivityResult:
|
|
110
|
+
return DefaultActivityResult.NEXT
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Activity:
|
|
114
|
+
"""
|
|
115
|
+
Sub classes must contain methods with a prededifined name and each must return a Result object.
|
|
116
|
+
Result code 0 should mean completed ok and should proceed normally
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def process(self, container: Container) -> ActivityResult:
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
|
|
126
|
+
def get_result_codes(self):
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class JSONObject(object):
|
|
131
|
+
JSON_DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
132
|
+
|
|
133
|
+
def date_to_json_format(self, dt):
|
|
134
|
+
return dt.strftime(self.JSON_DATE_TIME_FORMAT)
|
|
135
|
+
|
|
136
|
+
def default_parser(self, o):
|
|
137
|
+
"""
|
|
138
|
+
Default public parser for json
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(o, datetime.datetime):
|
|
141
|
+
return self.date_to_json_format(o)
|
|
142
|
+
if isinstance(o, enum.Enum):
|
|
143
|
+
return o.name
|
|
144
|
+
return o.__dict__
|
|
145
|
+
|
|
146
|
+
def get_parser(self):
|
|
147
|
+
return self.default_parser
|
|
148
|
+
|
|
149
|
+
def toJSON(self):
|
|
150
|
+
return json.dumps(self, default=self.get_parser(), sort_keys=True, indent=4)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ListData(JSONObject):
|
|
154
|
+
def __init__(self):
|
|
155
|
+
self._data = []
|
|
156
|
+
|
|
157
|
+
def add(self, entry):
|
|
158
|
+
if isinstance(entry, self.get_data_class()):
|
|
159
|
+
self._data.append(entry)
|
|
160
|
+
else:
|
|
161
|
+
raise exception.InvalidArgumentException("Instance does not match expected class.")
|
|
162
|
+
|
|
163
|
+
def is_empty(self):
|
|
164
|
+
return self.get_size() == 0
|
|
165
|
+
|
|
166
|
+
@abstractmethod
|
|
167
|
+
def get_data_class(self) -> type:
|
|
168
|
+
raise NotImplementedError
|
|
169
|
+
|
|
170
|
+
def iterator(self):
|
|
171
|
+
return iter(self._data)
|
|
172
|
+
|
|
173
|
+
def get_item_at(self, index: int):
|
|
174
|
+
return self._data[index]
|
|
175
|
+
|
|
176
|
+
def get_first(self):
|
|
177
|
+
return self.get_item_at(0)
|
|
178
|
+
|
|
179
|
+
def get_size(self):
|
|
180
|
+
return len(self._data)
|
|
181
|
+
|
|
182
|
+
def sort_data(self, sorter_lamda): # e.g. f = lambda h: h.name
|
|
183
|
+
sort_list = list(self._data)
|
|
184
|
+
sort_list.sort(key=sorter_lamda)
|
|
185
|
+
return iter(sort_list)
|
|
186
|
+
|
|
187
|
+
def map_to_list(self, map_function):
|
|
188
|
+
list_data = list(self._data)
|
|
189
|
+
return list(map(map_function, list_data))
|