Skip to content

Commit 3eb72e1

Browse files
GefMarSARomanchuk
authored andcommitted
add: Cast Callable for ProcessAttr (#29)
* add: Cast Callable for ProcessAttr * update: README.md ---------
1 parent 2076a92 commit 3eb72e1

File tree

3 files changed

+145
-58
lines changed

3 files changed

+145
-58
lines changed

README.md

Lines changed: 138 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,152 @@
1-
21
# logic_processes_layer
32

4-
The logic_processes_layer package provides a framework for structuring the logic of your Python programs in a flexible and maintainable way. It allows you to divide your program logic into separate processes, each of which can be easily modified or replaced without affecting the others.
3+
The `logic_processes_layer` package provides a framework for structuring the logic of your Python programs in a flexible and maintainable way. It allows you to divide your program logic into separate processes, each of which can be easily modified or replaced without affecting the others.
54

65
## Features
76

87
- **Separation of concerns**: Each process in your program can be developed and tested independently.
9-
108
- **Flexibility**: Processes can be easily added, removed, or modified without affecting the rest of your program.
11-
129
- **Ease of testing**: By isolating each process, you can write more focused and effective unit tests.
1310

1411
## Installation
1512

16-
You can install the logic_processes_layer package via pip:
13+
You can install the `logic_processes_layer` package via pip:
14+
15+
```bash
1716
pip install logic-processes-layer
17+
```
1818

1919
## New Features
2020

2121
- **ProcessAsSubprocess**: Use any process as a subprocess.
22-
- **InitMapper**: Simplifies process initialization with attribute mapping from the context.
22+
- **InitMapper**: Simplifies process initialization by mapping attributes from the context to processor arguments.
2323
- **ProcessAttr**: Retrieve attributes from the process context or directly from the process.
2424
- **Conditions Support**: Add logical conditions to control the execution of processes.
2525
- **AttrCondition**: Define conditions based on attributes of the process or context.
2626
- **FunctionCondition**: Wrap custom functions as conditions.
2727
- **Logical Operators**: Combine conditions with `&` (AND), `|` (OR), `~` (NOT), and `^` (XOR) for advanced logic.
28-
- [Examples](tests/examples) of how to use the logic_processes_layer package.
28+
- [Examples](tests/examples) of how to use the `logic_processes_layer` package.
29+
30+
### Using `ProcessAttr` and `InitMapper`
31+
32+
In many cases, you may want to initialize your process with specific values drawn from the current context or from the process itself. To simplify this, the package provides two utilities: `ProcessAttr` and `InitMapper`.
33+
34+
#### `ProcessAttr`
35+
36+
`ProcessAttr` makes it easy to fetch required attributes from the context or from the process. This class can be declared as generic (i.e., `Generic[AttrResultT]`) to enable strict typing if needed:
37+
38+
```python
39+
import dataclasses
40+
from operator import attrgetter
41+
import typing
42+
43+
44+
AnyTupleT = typing.Tuple[typing.Any, ...]
45+
DictStrAnyT = typing.Dict[str, typing.Any]
46+
AttrResultT = typing.TypeVar("AttrResultT")
47+
48+
49+
@dataclasses.dataclass
50+
class ProcessAttr(typing.Generic[AttrResultT]):
51+
attr_name: str
52+
from_context: bool = dataclasses.field(default=False)
53+
cast: typing.Callable[[typing.Any], AttrResultT] = dataclasses.field(default=lambda arg: arg)
54+
55+
def get_value(self, context: typing.Any) -> AttrResultT: # noqa: ANN401
56+
source = (context.process, context)[self.from_context]
57+
source_value = attrgetter(self.attr_name)(source)
58+
return self.cast(source_value)
59+
60+
```
61+
62+
- **`attr_name`**: The attribute name to retrieve.
63+
- **`from_context`**: A flag indicating whether to take the attribute from `context.process` (default) or directly from `context`.
64+
- **`cast`**: A callable to cast (transform) the retrieved value into a desired type (default simply returns the original value).
65+
66+
#### `InitMapper`
67+
68+
`InitMapper` helps you gather the needed `args` and `kwargs` for initializing a process or any other object. If certain values should come from the context, you can use `ProcessAttr` objects for those parameters.
69+
70+
Example:
71+
72+
```python
73+
from typing import Any
74+
import dataclasses
75+
76+
AnyTupleT = tuple[Any, ...]
77+
DictStrAnyT = dict[str, Any]
78+
79+
@dataclasses.dataclass
80+
class InitMapper:
81+
_init_attrs: tuple[Any, ...] = ()
82+
_init_kwargs: dict[str, Any] = dataclasses.field(default_factory=dict)
83+
84+
def __call__(self, context: Any) -> tuple[AnyTupleT, DictStrAnyT]:
85+
args = self._load_args(context)
86+
kwargs = self._load_kwargs(context)
87+
return args, kwargs
88+
89+
def _load_args(self, context: Any) -> AnyTupleT:
90+
args = []
91+
for init_attr in self._init_attrs:
92+
value = init_attr
93+
if isinstance(init_attr, ProcessAttr):
94+
value = init_attr.get_value(context)
95+
args.append(value)
96+
return tuple(args)
97+
98+
def _load_kwargs(self, context: Any) -> DictStrAnyT:
99+
kwargs = {}
100+
for key, value in self._init_kwargs.items():
101+
init_value = value
102+
if isinstance(value, ProcessAttr):
103+
init_value = value.get_value(context)
104+
kwargs[key] = init_value
105+
return kwargs
106+
```
29107

108+
- **`_init_attrs`**: A tuple of positional arguments to be passed to the constructor.
109+
- **`_init_kwargs`**: A dictionary of keyword arguments (key is the argument name, value is either a fixed value or a `ProcessAttr` to load from the context).
110+
111+
##### Usage Example
112+
113+
```python
114+
# Suppose we have a class MyProcessor that needs two arguments: name (str) and age (int).
115+
116+
class MyProcessor:
117+
def __init__(self, name: str, age: int) -> None:
118+
self.name = name
119+
self.age = age
120+
121+
def run(self):
122+
print(f"Name: {self.name}, Age: {self.age}")
123+
124+
# Assume the required values are stored in context.process, for example:
125+
# context.process.name = "Alice", context.process.age = 30
126+
127+
mapper = InitMapper(
128+
_init_attrs=(),
129+
_init_kwargs={
130+
"name": ProcessAttr[str]("name"),
131+
"age": ProcessAttr[int]("age")
132+
}
133+
)
134+
135+
# When mapper(context) is called, it will build (args, kwargs),
136+
# where args is an empty tuple and kwargs is {"name": "Alice", "age": 30}.
137+
138+
args, kwargs = mapper(context)
139+
processor = MyProcessor(*args, **kwargs)
140+
processor.run() # Prints: Name: Alice, Age: 30
141+
```
142+
143+
In this way, `InitMapper` provides a flexible mechanism for assembling parameters for object initialization, while `ProcessAttr` enables you to reference attributes from either the context or the process.
144+
145+
---
30146

31147
## Basic Usage
32148

33-
Here is a basic example of how to use the logic_processes_layer package:
149+
Below is a basic example of how to use the `logic_processes_layer` package, creating a process with pre- and post-run steps:
34150

35151
```python
36152
import dataclasses
@@ -61,84 +177,56 @@ process = MyClass()
61177
process()
62178
```
63179

64-
In this example, `MyClass` is a processor that has a pre-run step, a run step, and a post-run step. The pre-run step is performed by `MyPreProcess`, which saves a message in the context. The run step is defined in `MyClass` itself. The post-run step is performed by `MyPostProcess`, which retrieves the message from the context and prints it.
65-
66180
## Advanced Example: Processing Data from Multiple APIs
67181

68-
This example demonstrates how to use the logic_processes_layer package to process data from multiple APIs. The process is divided into three steps: pre-run, run, and post-run.
182+
This example demonstrates how to use the `logic_processes_layer` package to process data from multiple APIs. The process is divided into three steps: pre-run, run, and post-run.
69183

70184
### Pre-run
71185

72-
In the pre-run step, we have two subprocesses, each making a GET request to a different API. The responses from these requests are stored in `self.results.pre_run`, and will be used in the main run step.
73-
74186
```python
75187
class PreProcess1(BaseSubprocessor):
76-
77188
def __call__(self):
78-
# Assuming that we are making a GET request to the first API
189+
# Assuming a GET request to the first API
79190
response1 = requests.get('http://api1.com')
80-
# Return the response
81191
return response1.json()
82192

83193
class PreProcess2(BaseSubprocessor):
84-
85194
def __call__(self):
86-
# Assuming that we are making a GET request to the second API
195+
# Assuming a GET request to the second API
87196
response2 = requests.get('http://api2.com')
88-
# Return the response
89197
return response2.json()
90198
```
91199

92200
### Run
93201

94-
In the run step, we process the data from the pre-run step. The results from the two pre-run subprocesses are retrieved from `self.results.pre_run`, and we assume that these results are combined in some way by a hypothetical function `process_data`.
95-
96202
```python
97203
def run(self):
98-
# Process the data from the pre_run step
204+
# Retrieve and process the data from the pre_run step
99205
api1_response = self.results.pre_run[self.pre_run[0]]
100206
api2_response = self.results.pre_run[self.pre_run[1]]
101-
# Assume that we are combining the data from the two APIs in some way
102-
result = process_data(api1_response, api2_response) # process_data is a hypothetical function
103-
# Return the result
207+
result = process_data(api1_response, api2_response) # process_data is hypothetical
104208
return result
105209
```
106210

107211
### Post-run
108212

109-
In the post-run step, we have a subprocess that sends the result from the run step to another API. The response from this POST request is stored in `self.results.post_run`.
110-
111213
```python
112214
class PostProcess(BaseSubprocessor):
113-
114215
def __call__(self):
115216
result = context.process.results.run
116217
# Send the result to another API
117218
response3 = requests.post('http://api3.com', data=result)
118-
# Return the response
119219
return response3.json()
120220
```
121221

122-
Finally, we create an instance of our class and call it to execute the process:
123-
124-
```python
125-
process = MyClass()
126-
process()
127-
```
128-
129-
Please note that you need to replace `'http://api1.com'`, `'http://api2.com'`, and `'http://api3.com'` with the actual API URLs, and implement the `process_data` function that processes the data from the first two APIs.
130-
131222
## Advanced Example: ChainPipeline with Custom Mapper and Steps
132223

133-
In this example, we delve deeper into the use of `logic_processes_layer` and demonstrate how `ChainPipeline`, `AbstractMapper`, and `AbstractPipelineStep` can be used to create more complex processes.
134-
135-
1. **Processors**: We have three processors, `ProcessorOne`, `ProcessorTwo`, and `ProcessorThree`. Each of these processors performs a certain task and returns a result.
224+
In this example, we dive deeper into how `ChainPipeline`, `AbstractMapper`, and `AbstractPipelineStep` can be used to create more complex processes:
136225

137-
2. **Mappers**: We then have three mappers, `MapperOne`, `MapperTwo`, and `MapperThree`. These mappers are used to build attribute dictionaries that are passed into the processors.
138-
139-
3. **Steps**: We then have three steps, `StepOne`, `StepTwo`, and `StepThree`. Each of these steps uses one of the processors and one of the mappers.
140-
141-
4. **ChainPipeline**: Finally, we have a `ChainPipeline` that combines all three steps into one sequence.
226+
1. **Processors**: Three processors (`ProcessorOne`, `ProcessorTwo`, `ProcessorThree`), each performing a certain task and returning a result.
227+
2. **Mappers**: Three mappers (`MapperOne`, `MapperTwo`, `MapperThree`) to build attribute dictionaries to be passed into the processors.
228+
3. **Steps**: Three steps (`StepOne`, `StepTwo`, `StepThree`), each using one of the processors and one of the mappers.
229+
4. **ChainPipeline**: A `ChainPipeline` that combines all three steps into a sequence.
142230

143231
```python
144232
import dataclasses
@@ -180,16 +268,16 @@ class MapperOne(AbstractMapper):
180268
class MapperTwo(AbstractMapper):
181269
def build_attrs_strategy(self, prev_results):
182270
return AttrsData(
183-
args=self.start_attrs.args,
184-
kwargs={"data_for_init_two": prev_results["one"]}
271+
args=self.start_attrs.args,
272+
kwargs={"data_for_init_two": prev_results["one"]}
185273
)
186274

187275

188276
class MapperThree(AbstractMapper):
189277
def build_attrs_strategy(self, prev_results):
190278
return AttrsData(
191-
args=tuple(),
192-
kwargs={}
279+
args=tuple(),
280+
kwargs={}
193281
)
194282

195283

@@ -216,8 +304,4 @@ class ChainPipeline(AbstractChainPipeline):
216304
pipeline = ChainPipeline()
217305
result = pipeline()
218306
print(result)
219-
220307
```
221-
In this example, we create an instance of `ChainPipeline` and call it to execute the whole process.
222-
The result of each step is passed to the next step via the mapper,
223-
allowing each step to use the result of the previous step when building its attributes.

logic_processes_layer/extensions/mappers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@
1010

1111
AnyTupleT = typing.Tuple[typing.Any, ...]
1212
DictStrAnyT = typing.Dict[str, typing.Any]
13+
AttrResultT = typing.TypeVar("AttrResultT")
1314

1415

1516
@dataclasses.dataclass
16-
class ProcessAttr:
17+
class ProcessAttr(typing.Generic[AttrResultT]):
1718
attr_name: str
1819
from_context: bool = dataclasses.field(default=False)
20+
cast: typing.Callable[[typing.Any], AttrResultT] = dataclasses.field(default=lambda arg: arg)
1921

20-
def get_value(self, context: typing.Any) -> typing.Any: # noqa: ANN401
22+
def get_value(self, context: typing.Any) -> AttrResultT: # noqa: ANN401
2123
source = (context.process, context)[self.from_context]
22-
return attrgetter(self.attr_name)(source)
24+
source_value = attrgetter(self.attr_name)(source)
25+
return self.cast(source_value)
2326

2427

2528
class InitMapper:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "logic_processes_layer"
7-
version = "1.2025.01.06"
7+
version = "1.2025.01.09"
88
requires-python = ">=3.8"
99
description = "Abstractions for create business logic"
1010
readme = "README.md"

0 commit comments

Comments
 (0)