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.
@@ -0,0 +1,4 @@
1
+ *.pyc
2
+ .idea/
3
+ .DS_Store
4
+ dist*
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
@@ -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))