tiferet 1.0.0a18__tar.gz → 1.0.0b0__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.
- {tiferet-1.0.0a18/tiferet.egg-info → tiferet-1.0.0b0}/PKG-INFO +12 -3
- tiferet-1.0.0b0/README.md +560 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/setup.py +7 -4
- tiferet-1.0.0b0/tiferet/__init__.py +6 -0
- tiferet-1.0.0b0/tiferet/commands/__init__.py +6 -0
- tiferet-1.0.0b0/tiferet/commands/app.py +102 -0
- tiferet-1.0.0b0/tiferet/commands/core.py +124 -0
- tiferet-1.0.0b0/tiferet/commands/dependencies.py +76 -0
- tiferet-1.0.0b0/tiferet/commands/settings.py +101 -0
- tiferet-1.0.0b0/tiferet/configs/__init__.py +5 -0
- tiferet-1.0.0b0/tiferet/configs/error.py +48 -0
- tiferet-1.0.0b0/tiferet/configs/settings.py +37 -0
- tiferet-1.0.0b0/tiferet/contexts/app.py +322 -0
- tiferet-1.0.0b0/tiferet/contexts/cache.py +76 -0
- tiferet-1.0.0b0/tiferet/contexts/container.py +134 -0
- tiferet-1.0.0b0/tiferet/contexts/error.py +124 -0
- tiferet-1.0.0b0/tiferet/contexts/feature.py +160 -0
- tiferet-1.0.0b0/tiferet/contracts/__init__.py +4 -0
- tiferet-1.0.0b0/tiferet/contracts/app.py +92 -0
- tiferet-1.0.0b0/tiferet/contracts/cache.py +70 -0
- tiferet-1.0.0b0/tiferet/contracts/container.py +147 -0
- tiferet-1.0.0b0/tiferet/contracts/error.py +177 -0
- tiferet-1.0.0b0/tiferet/contracts/feature.py +159 -0
- tiferet-1.0.0b0/tiferet/contracts/settings.py +34 -0
- tiferet-1.0.0b0/tiferet/data/__init__.py +4 -0
- tiferet-1.0.0b0/tiferet/data/app.py +148 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/data/container.py +52 -38
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/data/error.py +15 -24
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/data/feature.py +27 -8
- tiferet-1.0.0a18/tiferet/domain/core.py → tiferet-1.0.0b0/tiferet/data/settings.py +36 -96
- tiferet-1.0.0b0/tiferet/domain/app.py +6 -0
- tiferet-1.0.0b0/tiferet/domain/core.py +102 -0
- tiferet-1.0.0b0/tiferet/domain/feature.py +4 -0
- tiferet-1.0.0b0/tiferet/handlers/__init__.py +0 -0
- tiferet-1.0.0b0/tiferet/handlers/container.py +116 -0
- tiferet-1.0.0b0/tiferet/handlers/error.py +49 -0
- tiferet-1.0.0b0/tiferet/handlers/feature.py +94 -0
- tiferet-1.0.0b0/tiferet/models/__init__.py +4 -0
- tiferet-1.0.0b0/tiferet/models/app.py +150 -0
- tiferet-1.0.0b0/tiferet/models/container.py +135 -0
- tiferet-1.0.0b0/tiferet/models/error.py +204 -0
- {tiferet-1.0.0a18/tiferet/domain → tiferet-1.0.0b0/tiferet/models}/feature.py +107 -47
- tiferet-1.0.0b0/tiferet/models/settings.py +148 -0
- tiferet-1.0.0b0/tiferet/proxies/__init__.py +0 -0
- tiferet-1.0.0b0/tiferet/proxies/yaml/__init__.py +0 -0
- {tiferet-1.0.0a18/tiferet/repos → tiferet-1.0.0b0/tiferet/proxies/yaml}/app.py +13 -41
- {tiferet-1.0.0a18/tiferet/repos → tiferet-1.0.0b0/tiferet/proxies/yaml}/container.py +26 -56
- {tiferet-1.0.0a18/tiferet/repos → tiferet-1.0.0b0/tiferet/proxies/yaml}/error.py +11 -70
- tiferet-1.0.0b0/tiferet/proxies/yaml/feature.py +92 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0/tiferet.egg-info}/PKG-INFO +12 -3
- tiferet-1.0.0b0/tiferet.egg-info/SOURCES.txt +60 -0
- tiferet-1.0.0a18/tiferet/__init__.py +0 -5
- tiferet-1.0.0a18/tiferet/commands/container.py +0 -54
- tiferet-1.0.0a18/tiferet/commands/error.py +0 -21
- tiferet-1.0.0a18/tiferet/commands/feature.py +0 -90
- tiferet-1.0.0a18/tiferet/configs/__init__.py +0 -69
- tiferet-1.0.0a18/tiferet/contexts/__init__.py +0 -8
- tiferet-1.0.0a18/tiferet/contexts/app.py +0 -289
- tiferet-1.0.0a18/tiferet/contexts/container.py +0 -232
- tiferet-1.0.0a18/tiferet/contexts/error.py +0 -121
- tiferet-1.0.0a18/tiferet/contexts/feature.py +0 -123
- tiferet-1.0.0a18/tiferet/contexts/request.py +0 -110
- tiferet-1.0.0a18/tiferet/data/__init__.py +0 -5
- tiferet-1.0.0a18/tiferet/data/app.py +0 -285
- tiferet-1.0.0a18/tiferet/domain/app.py +0 -131
- tiferet-1.0.0a18/tiferet/repos/__init__.py +0 -7
- tiferet-1.0.0a18/tiferet/repos/feature.py +0 -151
- tiferet-1.0.0a18/tiferet.egg-info/SOURCES.txt +0 -37
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/LICENSE +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/setup.cfg +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/clients/__init__.py +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/clients/yaml.py +0 -0
- {tiferet-1.0.0a18/tiferet/commands → tiferet-1.0.0b0/tiferet/contexts}/__init__.py +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/domain/__init__.py +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/domain/container.py +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet/domain/error.py +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet.egg-info/dependency_links.txt +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet.egg-info/requires.txt +0 -0
- {tiferet-1.0.0a18 → tiferet-1.0.0b0}/tiferet.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: tiferet
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0b0
|
4
4
|
Summary: A multi-purpose application framework embodying beauty in form.
|
5
5
|
Home-page: https://github.com/greatstrength/app
|
6
6
|
Download-URL: https://github.com/greatstrength/app
|
7
|
-
Author: Andrew Shatz
|
7
|
+
Author: Andrew Shatz, Great Strength Systems
|
8
8
|
Author-email: andrew@greatstrength.me
|
9
9
|
License: BSD 3
|
10
10
|
License-File: LICENSE
|
@@ -14,3 +14,12 @@ Requires-Dist: dependencies>=7.7.0
|
|
14
14
|
Provides-Extra: test
|
15
15
|
Requires-Dist: pytest>=8.3.3; extra == "test"
|
16
16
|
Requires-Dist: pytest_env>=1.1.5; extra == "test"
|
17
|
+
Dynamic: author
|
18
|
+
Dynamic: author-email
|
19
|
+
Dynamic: download-url
|
20
|
+
Dynamic: home-page
|
21
|
+
Dynamic: license
|
22
|
+
Dynamic: license-file
|
23
|
+
Dynamic: provides-extra
|
24
|
+
Dynamic: requires-dist
|
25
|
+
Dynamic: summary
|
@@ -0,0 +1,560 @@
|
|
1
|
+
# Tiferet - A Python Framework for Domain-Driven Design
|
2
|
+
|
3
|
+
## Introduction
|
4
|
+
|
5
|
+
Tiferet is a Python framework that elegantly distills Domain-Driven Design (DDD) into a practical, powerful tool. Drawing inspiration from the Kabbalistic concept of beauty in balance, Tiferet weaves purpose and functionality into software that not only performs but resonates deeply with its intended vision. As a cornerstone for crafting diverse applications, Tiferet empowers developers to build solutions with clarity, grace, and thoughtful design.
|
6
|
+
|
7
|
+
Tiferet embraces the complexity of real-world processes through DDD, transforming intricate business logic and evolving requirements into clear, manageable models. Far from merely navigating this labyrinth, Tiferet provides a graceful path to craft software that reflects its intended purpose with wisdom and precision, embodying the Kabbalistic beauty of balanced form and function. This tutorial guides you through building a simple calculator application, demonstrating how Tiferet harmonizes code and concept. By defining a domain model, command classes, and feature-driven configurations, you’ll create a robust and extensible calculator that resonates with Tiferet’s philosophy.
|
8
|
+
|
9
|
+
## Getting Started with Tiferet
|
10
|
+
Embark on your Tiferet journey with a few simple steps to set up your Python environment. Whether you're new to Python or a seasoned developer, these instructions will prepare you to craft a calculator application with grace and precision.
|
11
|
+
|
12
|
+
### Installing Python
|
13
|
+
Tiferet requires Python 3.10 or later. Follow these steps to install it:
|
14
|
+
|
15
|
+
#### Windows
|
16
|
+
|
17
|
+
Visit python.org, navigate to the Downloads section, and select the Python 3.10 installer for Windows.
|
18
|
+
Run the installer, ensuring you check "Add Python 3.10 to PATH," then click "Install Now."
|
19
|
+
|
20
|
+
#### macOS
|
21
|
+
|
22
|
+
Download the Python 3.10 installer from python.org.
|
23
|
+
Open the .pkg file and follow the installation prompts.
|
24
|
+
|
25
|
+
#### Linux (Ubuntu/Debian)
|
26
|
+
```bash
|
27
|
+
sudo apt update
|
28
|
+
sudo apt install software-properties-common
|
29
|
+
sudo add-apt-repository ppa:deadsnakes/ppa
|
30
|
+
sudo apt update
|
31
|
+
sudo apt install python3.10
|
32
|
+
```
|
33
|
+
|
34
|
+
#### Verify the installation by running:
|
35
|
+
```bash
|
36
|
+
python3.10 --version
|
37
|
+
```
|
38
|
+
|
39
|
+
You should see Python 3.10.x if successful.
|
40
|
+
|
41
|
+
### Setting Up a Virtual Environment
|
42
|
+
To keep your project dependencies organized, create a virtual environment named tiferet_app for your calculator application:
|
43
|
+
|
44
|
+
#### Create the Environment
|
45
|
+
|
46
|
+
```bash
|
47
|
+
# Windows
|
48
|
+
python -m venv tiferet_app
|
49
|
+
|
50
|
+
# macOS/Linux
|
51
|
+
python3.10 -m venv tiferet_app
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Activate the Environment
|
55
|
+
Activate the environment to isolate your project's dependencies:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
# Windows (Command Prompt)
|
59
|
+
tiferet_app\Scripts\activate
|
60
|
+
|
61
|
+
# Windows (PowerShell)
|
62
|
+
.\tiferet_app\Scripts\Activate.ps1
|
63
|
+
|
64
|
+
# macOS/Linux
|
65
|
+
source tiferet_app/bin/activate
|
66
|
+
```
|
67
|
+
|
68
|
+
Your terminal should display (tiferet_app), confirming the environment is active. You can now install Tiferet and other dependencies without affecting your system’s Python setup.
|
69
|
+
Deactivate the Environment
|
70
|
+
When finished, deactivate the environment with:
|
71
|
+
deactivate
|
72
|
+
|
73
|
+
## Your First Calculator App
|
74
|
+
With your tiferet_app virtual environment activated, you're ready to install Tiferet and start building your calculator application. Follow these steps to set up your project and begin crafting with Tiferet’s elegant approach.
|
75
|
+
|
76
|
+
### Installing Tiferet
|
77
|
+
Install the Tiferet package using pip in your activated virtual environment:
|
78
|
+
|
79
|
+
```bash
|
80
|
+
# Windows
|
81
|
+
pip install tiferet
|
82
|
+
|
83
|
+
# macOS/Linux
|
84
|
+
pip3 install tiferet
|
85
|
+
```
|
86
|
+
|
87
|
+
### Project Structure
|
88
|
+
Create a project directory structure to organize your calculator application:
|
89
|
+
|
90
|
+
```plaintext
|
91
|
+
project_root/
|
92
|
+
├── basic_calc.py
|
93
|
+
└── app/
|
94
|
+
├── commands/
|
95
|
+
│ ├── __init__.py
|
96
|
+
│ ├── calc.py
|
97
|
+
│ └── valid.py
|
98
|
+
├── configs/
|
99
|
+
│ ├── __init__.py
|
100
|
+
│ └── config.yaml
|
101
|
+
└── models/
|
102
|
+
├── __init__.py
|
103
|
+
└── calc.py
|
104
|
+
```
|
105
|
+
|
106
|
+
The app/models/ directory will house the calculator’s domain model, app/commands/ will contain command classes for operations and validations, and app/configs/ will store configuration files. The basic_calc.py script at the root will initialize and run the application. While the app directory name is customizable for package releases, we recommend retaining it for internal or proprietary projects to maintain simplicity and consistency.
|
107
|
+
|
108
|
+
## Crafting the Calculator Application
|
109
|
+
With Tiferet installed and your project structured, it’s time to bring your calculator application to life. We’ll start by defining the domain model, then create command classes for arithmetic and validation, configure the application’s behavior through container attributes, features, errors, and context, and finally initialize and demonstrate the app with a script. This sequence showcases Tiferet’s harmonious design, weaving together models, commands, and configurations with grace.
|
110
|
+
|
111
|
+
### Defining the Number Model in models/calc.py
|
112
|
+
The calculator’s numerical values are represented by a Number value object, defined in app/models/calc.py. This model encapsulates a string-based numerical value, validated to ensure it represents an integer or float, and provides a method to format it as a number.
|
113
|
+
|
114
|
+
Create app/models/calc.py with the following content:
|
115
|
+
|
116
|
+
```python
|
117
|
+
from tiferet.models import *
|
118
|
+
|
119
|
+
class Number(ValueObject):
|
120
|
+
'''
|
121
|
+
A value object representing a numerical value in the calculator domain.
|
122
|
+
'''
|
123
|
+
value = StringType(
|
124
|
+
required=True,
|
125
|
+
regex=r'^-?\d*\.?\d*$',
|
126
|
+
metadata=dict(
|
127
|
+
description='A string representing an integer or float (e.g., "123", "-123.45", "0123", ".123", "123.").'
|
128
|
+
)
|
129
|
+
)
|
130
|
+
|
131
|
+
def is_float(self) -> bool:
|
132
|
+
'''
|
133
|
+
Check if the value is formatted as a float.
|
134
|
+
|
135
|
+
:return: True if the value contains a decimal point and valid digits, False otherwise.
|
136
|
+
'''
|
137
|
+
return '.' in self.value and self.value.strip('-.').replace('.', '').isdigit()
|
138
|
+
|
139
|
+
def format(self) -> int | float:
|
140
|
+
'''
|
141
|
+
Convert the string value to an integer or float.
|
142
|
+
|
143
|
+
:return: An integer if the value represents a whole number, otherwise a float.
|
144
|
+
'''
|
145
|
+
if self.is_float():
|
146
|
+
return float(self.value)
|
147
|
+
return int(self.value)
|
148
|
+
```
|
149
|
+
|
150
|
+
The Number class uses Tiferet’s ValueObject to ensure immutability, with a StringType attribute validated by a regex to accept integers and floats (e.g., "123", "-123.45", ".123"). The is_float method checks for decimal points, and format converts the string to an int or float, enabling arithmetic operations.
|
151
|
+
|
152
|
+
### Defining Command Classes in commands/calc.py and commands/valid.py
|
153
|
+
Next, we define command classes to perform arithmetic operations and input validation. Arithmetic commands (AddNumber, SubtractNumber, MultiplyNumber, DivideNumber, ExponentiateNumber) are in app/commands/calc.py, while the validation command (ValidateNumber) is in app/commands/valid.py. All inherit from Tiferet’s Command base class, using the static Command.handle method for execution.
|
154
|
+
|
155
|
+
#### Arithmetic Commands in commands/calc.py
|
156
|
+
Create app/commands/calc.py with the following content:
|
157
|
+
|
158
|
+
```python
|
159
|
+
from ..commands import Command
|
160
|
+
from ..models import ModelObject
|
161
|
+
from ..models.calc import Number
|
162
|
+
|
163
|
+
class AddNumber(Command):
|
164
|
+
'''
|
165
|
+
A command to perform addition of two numbers.
|
166
|
+
'''
|
167
|
+
def execute(self, a: Number, b: Number) -> Number:
|
168
|
+
'''
|
169
|
+
Execute the addition command.
|
170
|
+
|
171
|
+
:param a: A Number object representing the first number.
|
172
|
+
:type a: Number
|
173
|
+
:param b: A Number object representing the second number.
|
174
|
+
:type b: Number
|
175
|
+
:return: A Number object representing the sum of a and b.
|
176
|
+
:rtype: Number
|
177
|
+
'''
|
178
|
+
return ModelObject.new(Number, value=str(a.format() + b.format()))
|
179
|
+
|
180
|
+
class SubtractNumber(Command):
|
181
|
+
'''
|
182
|
+
A command to perform subtraction of two numbers.
|
183
|
+
'''
|
184
|
+
def execute(self, a: Number, b: Number) -> Number:
|
185
|
+
'''
|
186
|
+
Execute the subtraction command.
|
187
|
+
|
188
|
+
:param a: A Number object representing the first number.
|
189
|
+
:type a: Number
|
190
|
+
:param b: A Number object representing the second number.
|
191
|
+
:type b: Number
|
192
|
+
:return: A Number object representing the difference of a and b.
|
193
|
+
:rtype: Number
|
194
|
+
'''
|
195
|
+
return ModelObject.new(Number, value=str(a.format() - b.format()))
|
196
|
+
|
197
|
+
class MultiplyNumber(Command):
|
198
|
+
'''
|
199
|
+
A command to perform multiplication of two numbers.
|
200
|
+
'''
|
201
|
+
def execute(self, a: Number, b: Number) -> Number:
|
202
|
+
'''
|
203
|
+
Execute the multiplication command.
|
204
|
+
|
205
|
+
:param a: A Number object representing the first number.
|
206
|
+
:type a: Number
|
207
|
+
:param b: A Number object representing the second number.
|
208
|
+
:type b: Number
|
209
|
+
:return: A Number object representing the product of a and b.
|
210
|
+
:rtype: Number
|
211
|
+
'''
|
212
|
+
return ModelObject.new(Number, value=str(a.format() * b.format()))
|
213
|
+
|
214
|
+
class DivideNumber(Command):
|
215
|
+
'''
|
216
|
+
A command to perform division of two numbers.
|
217
|
+
'''
|
218
|
+
def execute(self, a: Number, b: Number) -> Number:
|
219
|
+
'''
|
220
|
+
Execute the division command.
|
221
|
+
|
222
|
+
:param a: A Number object representing the first number.
|
223
|
+
:type a: Number
|
224
|
+
:param b: A Number object representing the second number, must be non-zero.
|
225
|
+
:type b: Number
|
226
|
+
:return: A Number object representing the quotient of a and b.
|
227
|
+
:rtype: Number
|
228
|
+
'''
|
229
|
+
self.verify(b.format() != 0, 'DIVISION_BY_ZERO')
|
230
|
+
return ModelObject.new(Number, value=str(a.format() / b.format()))
|
231
|
+
|
232
|
+
class ExponentiateNumber(Command):
|
233
|
+
'''
|
234
|
+
A command to perform exponentiation of two numbers.
|
235
|
+
'''
|
236
|
+
def execute(self, a: Number, b: Number) -> Number:
|
237
|
+
'''
|
238
|
+
Execute the exponentiation command.
|
239
|
+
|
240
|
+
:param a: A Number object representing the base number.
|
241
|
+
:type a: Number
|
242
|
+
:param b: A Number object representing the exponent.
|
243
|
+
:type b: Number
|
244
|
+
:return: A Number object representing a raised to the power of b.
|
245
|
+
:rtype: Number
|
246
|
+
'''
|
247
|
+
return ModelObject.new(Number, value=str(a.format() ** b.format()))
|
248
|
+
```
|
249
|
+
|
250
|
+
These commands perform arithmetic operations on Number objects, using format() to extract numerical values and ModelObject.new to return results as new Number objects. The DivideNumber command includes a verify check to prevent division by zero, referencing a configured error.
|
251
|
+
|
252
|
+
#### Validation Command in commands/valid.py
|
253
|
+
Create app/commands/valid.py with the following content:
|
254
|
+
|
255
|
+
```python
|
256
|
+
from ..commands import Command
|
257
|
+
from ..models.calc import Number
|
258
|
+
|
259
|
+
class ValidateNumber(Command):
|
260
|
+
'''
|
261
|
+
A command to validate that a value can be a Number object.
|
262
|
+
'''
|
263
|
+
def execute(self, value: str) -> None:
|
264
|
+
'''
|
265
|
+
Validate that the input value can be used to create a Number object.
|
266
|
+
|
267
|
+
:param value: Any string value to validate.
|
268
|
+
:type value: str
|
269
|
+
:raises TiferetError: If the value cannot be a Number.
|
270
|
+
'''
|
271
|
+
try:
|
272
|
+
ModelObject.new(Number, value=str(value))
|
273
|
+
except Exception as e:
|
274
|
+
self.verify(False, 'INVALID_INPUT', value)
|
275
|
+
```
|
276
|
+
|
277
|
+
The ValidateNumber command ensures inputs can be converted to Number objects, raising a configured error for invalid values.
|
278
|
+
|
279
|
+
### Configuring the Application in configs/config.yaml
|
280
|
+
The calculator’s behavior is defined in app/configs/config.yaml, which configures container attributes, features, errors, and the application context. This centralized configuration enables Tiferet’s dependency injection container to orchestrate commands and features gracefully.
|
281
|
+
|
282
|
+
Create `app/configs/config.yaml` with the following content:
|
283
|
+
|
284
|
+
```yaml
|
285
|
+
attrs:
|
286
|
+
add_number_cmd:
|
287
|
+
module_path: app.commands.calc
|
288
|
+
class_name: AddNumber
|
289
|
+
subtract_number_cmd:
|
290
|
+
module_path: app.commands.calc
|
291
|
+
class_name: SubtractNumber
|
292
|
+
multiply_number_cmd:
|
293
|
+
module_path: app.commands.calc
|
294
|
+
class_name: MultiplyNumber
|
295
|
+
divide_number_cmd:
|
296
|
+
module_path: app.commands.calc
|
297
|
+
class_name: DivideNumber
|
298
|
+
exponentiate_number_cmd:
|
299
|
+
module_path: app.commands.calc
|
300
|
+
class_name: ExponentiateNumber
|
301
|
+
validate_number_cmd:
|
302
|
+
module_path: app.commands.valid
|
303
|
+
class_name: ValidateNumber
|
304
|
+
|
305
|
+
features:
|
306
|
+
calc.add:
|
307
|
+
name: 'Add Number'
|
308
|
+
description: 'Adds one number to another'
|
309
|
+
commands:
|
310
|
+
- attribute_id: validate_number_cmd
|
311
|
+
name: Validate `a` input
|
312
|
+
data_key: a
|
313
|
+
params:
|
314
|
+
value: $r.a
|
315
|
+
- attribute_id: validate_number_cmd
|
316
|
+
name: Validate `b` input
|
317
|
+
data_key: b
|
318
|
+
params:
|
319
|
+
value: $r.b
|
320
|
+
- attribute_id: add_number_cmd
|
321
|
+
name: Add `a` and `b`
|
322
|
+
calc.subtract:
|
323
|
+
name: 'Subtract Number'
|
324
|
+
description: 'Subtracts one number from another'
|
325
|
+
commands:
|
326
|
+
- attribute_id: validate_number_cmd
|
327
|
+
name: Validate `a` input
|
328
|
+
data_key: a
|
329
|
+
params:
|
330
|
+
value: $r.a
|
331
|
+
- attribute_id: validate_number_cmd
|
332
|
+
name: Validate `b` input
|
333
|
+
data_key: b
|
334
|
+
params:
|
335
|
+
value: $r.b
|
336
|
+
- attribute_id: subtract_number_cmd
|
337
|
+
name: Subtract `b` from `a`
|
338
|
+
calc.multiply:
|
339
|
+
name: 'Multiply Number'
|
340
|
+
description: 'Multiplies one number by another'
|
341
|
+
commands:
|
342
|
+
- attribute_id: validate_number_cmd
|
343
|
+
name: Validate `a` input
|
344
|
+
data_key: a
|
345
|
+
params:
|
346
|
+
value: $r.a
|
347
|
+
- attribute_id: validate_number_cmd
|
348
|
+
name: Validate `b` input
|
349
|
+
data_key: b
|
350
|
+
params:
|
351
|
+
value: $r.b
|
352
|
+
- attribute_id: multiply_number_cmd
|
353
|
+
name: Multiply `a` and `b`
|
354
|
+
calc.divide:
|
355
|
+
name: 'Divide Number'
|
356
|
+
description: 'Divides one number by another'
|
357
|
+
commands:
|
358
|
+
- attribute_id: validate_number_cmd
|
359
|
+
name: Validate `a` input
|
360
|
+
data_key: a
|
361
|
+
params:
|
362
|
+
value: $r.a
|
363
|
+
- attribute_id: validate_number_cmd
|
364
|
+
name: Validate `b` input
|
365
|
+
data_key: b
|
366
|
+
params:
|
367
|
+
value: $r.b
|
368
|
+
- attribute_id: divide_number_cmd
|
369
|
+
name: Divide `a` by `b`
|
370
|
+
calc.exp:
|
371
|
+
name: 'Exponentiate Number'
|
372
|
+
description: 'Raises one number to the power of another'
|
373
|
+
commands:
|
374
|
+
- attribute_id: validate_number_cmd
|
375
|
+
name: Validate `a` input
|
376
|
+
data_key: a
|
377
|
+
params:
|
378
|
+
value: $r.a
|
379
|
+
- attribute_id: validate_number_cmd
|
380
|
+
name: Validate `b` input
|
381
|
+
data_key: b
|
382
|
+
params:
|
383
|
+
value: $r.b
|
384
|
+
- attribute_id: exponentiate_number_cmd
|
385
|
+
name: Raise `a` to the power of `b`
|
386
|
+
calc.sqrt:
|
387
|
+
name: 'Square Root'
|
388
|
+
description: 'Calculates the square root of a number'
|
389
|
+
commands:
|
390
|
+
- attribute_id: validate_number_cmd
|
391
|
+
name: Validate `a` input
|
392
|
+
data_key: a
|
393
|
+
params:
|
394
|
+
value: $r.a
|
395
|
+
- attribute_id: validate_number_cmd
|
396
|
+
name: Convert `b` to number
|
397
|
+
data_key: b
|
398
|
+
params:
|
399
|
+
value: '0.5'
|
400
|
+
- attribute_id: exponentiate_number_cmd
|
401
|
+
name: Calculate square root of `a`
|
402
|
+
|
403
|
+
errors:
|
404
|
+
invalid_input:
|
405
|
+
name: Invalid Numeric Input
|
406
|
+
message:
|
407
|
+
- lang: en_US
|
408
|
+
text: 'Value {} must be a number'
|
409
|
+
- lang: es_ES
|
410
|
+
text: 'El valor {} debe ser un número'
|
411
|
+
division_by_zero:
|
412
|
+
name: Division By Zero
|
413
|
+
message:
|
414
|
+
- lang: en_US
|
415
|
+
text: 'Cannot divide by zero'
|
416
|
+
- lang: es_ES
|
417
|
+
text: 'No se puede dividir por cero'
|
418
|
+
|
419
|
+
contexts:
|
420
|
+
basic_calc:
|
421
|
+
name: Basic Calculator
|
422
|
+
description: Perform basic calculator operations
|
423
|
+
const:
|
424
|
+
container_config_file: 'app/configs/config.yaml'
|
425
|
+
feature_config_file: 'app/configs/config.yaml'
|
426
|
+
error_config_file: 'app/configs/config.yaml'
|
427
|
+
```
|
428
|
+
|
429
|
+
attrs: Defines container attributes for dependency injection, mapping to command classes (e.g., add_number_cmd to AddNumber).
|
430
|
+
|
431
|
+
|
432
|
+
features: Configures feature workflows, sequencing validation and arithmetic commands (e.g., calc.add validates a and b, then adds them). The calc.sqrt feature reuses exponentiate_number_cmd with b: "0.5" for square roots.
|
433
|
+
|
434
|
+
errors: Specifies error messages for invalid_input and division_by_zero, supporting en_US and es_ES for multilingual extensibility.
|
435
|
+
|
436
|
+
contexts: Defines the basic_calc application instance, linking to the configuration file for container, features, and errors.
|
437
|
+
|
438
|
+
### Initializing and Demonstrating the Calculator in basic_calc.py
|
439
|
+
Finally, we initialize the calculator with an initializer script, basic_calc.py, at the project root. This script uses Tiferet’s App class to load the basic_calc context and execute features, demonstrating the calculator’s functionality.
|
440
|
+
Create basic_calc.py with the following content:
|
441
|
+
|
442
|
+
```python
|
443
|
+
from tiferet import App
|
444
|
+
|
445
|
+
# Create new app (manager) instance.
|
446
|
+
app = App(config_file='app/configs/config.yaml')
|
447
|
+
|
448
|
+
# Execute the add feature to add the values.
|
449
|
+
a = 1
|
450
|
+
b = 2
|
451
|
+
addition = app.execute_feature('basic_calc', 'calc.add', a=str(a), b=str(b))
|
452
|
+
|
453
|
+
print(f'{a} + {b} = {addition.format()}')
|
454
|
+
```
|
455
|
+
|
456
|
+
### Demonstrating the Calculator
|
457
|
+
To run the calculator, ensure your tiferet_app virtual environment is activated and Tiferet is installed. Execute the initializer script:
|
458
|
+
```bash
|
459
|
+
python basic_calc
|
460
|
+
```
|
461
|
+
|
462
|
+
### Running the Calculator as a CLI
|
463
|
+
For a flexible and scriptable interface, the calculator includes a command-line interface (CLI) implemented in calc_cli.py at the project root. This script complements the basic_calc.py test script, which remains available for debugging and simple feature execution. The calc_cli.py script leverages Tiferet’s App class to execute features defined in app/configs/config.yaml, accepting command-line arguments for operations, input values, and locale selection. It supports all calculator features: addition (calc.add), subtraction (calc.subtract), multiplication (calc.multiply), division (calc.divide), exponentiation (calc.exp), and square root (calc.sqrt).
|
464
|
+
The calc_cli.py script uses Python’s argparse to define subcommands for each feature, with required arguments -a (first number) and -b (second number, except for sqrt). An optional --locale flag selects the error message language (en_US or es_ES, defaulting to en_US). The script executes the specified feature in the basic_calc context, returning a Number object whose format() method yields the result as an integer or float.
|
465
|
+
|
466
|
+
Create calc_cli.py with the following content:
|
467
|
+
```python
|
468
|
+
import argparse
|
469
|
+
from tiferet import App, TiferetError
|
470
|
+
|
471
|
+
def main():
|
472
|
+
"""Parse CLI arguments and execute the calculator feature."""
|
473
|
+
parser = argparse.ArgumentParser(description="Basic Calculator CLI using Tiferet")
|
474
|
+
parser.add_argument('--config', default='app/configs/config.yaml', help='Path to config file')
|
475
|
+
parser.add_argument('--locale', default='en_US', choices=['en_US', 'es_ES'], help='Language for error messages')
|
476
|
+
|
477
|
+
subparsers = parser.add_subparsers(dest='operation', required=True, help='Calculator operation')
|
478
|
+
|
479
|
+
# Define subcommands for each feature
|
480
|
+
operations = [
|
481
|
+
('add', 'Add two numbers', True),
|
482
|
+
('subtract', 'Subtract one number from another', True),
|
483
|
+
('multiply', 'Multiply two numbers', True),
|
484
|
+
('divide', 'Divide one number by another', True),
|
485
|
+
('exp', 'Raise one number to the power of another', True),
|
486
|
+
('sqrt', 'Calculate the square root of a number', False),
|
487
|
+
]
|
488
|
+
|
489
|
+
for op, help_text, needs_b in operations:
|
490
|
+
subparser = subparsers.add_parser(op, help=help_text)
|
491
|
+
subparser.add_argument('-a', required=True, help='First number')
|
492
|
+
if needs_b:
|
493
|
+
subparser.add_argument('-b', required=True, help='Second number')
|
494
|
+
|
495
|
+
args = parser.parse_args()
|
496
|
+
|
497
|
+
# Create app instance
|
498
|
+
app = App(config_file=args.config)
|
499
|
+
|
500
|
+
# Map operation to feature ID
|
501
|
+
feature_map = {
|
502
|
+
'add': 'calc.add',
|
503
|
+
'subtract': 'calc.subtract',
|
504
|
+
'multiply': 'calc.multiply',
|
505
|
+
'divide': 'calc.divide',
|
506
|
+
'exp': 'calc.exp',
|
507
|
+
'sqrt': 'calc.sqrt',
|
508
|
+
}
|
509
|
+
feature_id = feature_map[args.operation]
|
510
|
+
|
511
|
+
# Prepare feature parameters
|
512
|
+
params = {'a': str(args.a)}
|
513
|
+
if args.operation != 'sqrt':
|
514
|
+
params['b'] = str(args.b)
|
515
|
+
|
516
|
+
try:
|
517
|
+
# Execute feature with locale
|
518
|
+
result = app.execute_feature('basic_calc', feature_id, locale=args.locale, **params)
|
519
|
+
|
520
|
+
# Display result
|
521
|
+
if args.operation == 'sqrt':
|
522
|
+
print(f"√{args.a} = {result.format()}")
|
523
|
+
else:
|
524
|
+
op_symbol = {'add': '+', 'subtract': '-', 'multiply': '*', 'divide': '/', 'exp': '^'}[args.operation]
|
525
|
+
print(f"{args.a} {op_symbol} {args.b} = {result.format()}")
|
526
|
+
except TiferetError as e:
|
527
|
+
print(f"Error: {e.message}")
|
528
|
+
except Exception as e:
|
529
|
+
print(f"Unexpected error: {str(e)}")
|
530
|
+
|
531
|
+
if __name__ == '__main__':
|
532
|
+
main()
|
533
|
+
```
|
534
|
+
|
535
|
+
Run the CLI with commands like:
|
536
|
+
```bash
|
537
|
+
# Add two numbers (default en_US)
|
538
|
+
python calc_cli.py add -a 1 -b 2
|
539
|
+
# Output: 1 + 2 = 3
|
540
|
+
|
541
|
+
# Calculate square root (en_US)
|
542
|
+
python calc_cli.py sqrt -a 4
|
543
|
+
# Output: √4 = 2.0
|
544
|
+
|
545
|
+
# Invalid input (es_ES)
|
546
|
+
python calc_cli.py add -a abc -b 2 --locale es_ES
|
547
|
+
# Output: Error: El valor abc debe ser un número
|
548
|
+
|
549
|
+
# Division by zero (en_US)
|
550
|
+
python calc_cli.py divide -a 5 -b 0
|
551
|
+
# Output: Error: Cannot divide by zero
|
552
|
+
```
|
553
|
+
|
554
|
+
The calc_cli.py script is scriptable and integrates easily with shell scripts or external systems, making it a versatile interface for the calculator. For quick testing or debugging, use basic_calc.py, which executes a single feature (e.g., calc.add) with hardcoded values. The CLI’s argument-driven design allows precise control over operations, with error messages tailored to the selected locale, showcasing Tiferet’s multilingual capabilities.
|
555
|
+
|
556
|
+
## Conclusion
|
557
|
+
This tutorial has woven together the elegance of Tiferet’s Domain-Driven Design framework to create a robust and extensible basic calculator. From defining the immutable Number model to crafting command classes for arithmetic and validation, configuring features and errors, and launching the application via both a test script (basic_calc.py) and a CLI (calc_cli.py), you’ve experienced Tiferet’s balance of clarity and power. The configuration-driven approach, with dependency injection and multilingual error handling, embodies the Kabbalistic beauty of purposeful design, making the calculator both functional and a joy to develop.
|
558
|
+
|
559
|
+
With the foundation laid, you can extend this application in many directions. Consider adding a terminal user interface (TUI) in a new script, calc_tui.py, to wrap calc_cli.py for interactive menu-driven operation. Explore a scientific calculator context (sci_calc) with advanced features like trigonometric functions, reusing the Number model or introducing new ones. Or integrate the calculator into larger systems, leveraging Tiferet’s modularity for domains like financial modeling or data processing. Whatever path you choose, Tiferet’s graceful framework will guide you to solutions that resonate with both purpose and precision.
|
560
|
+
To continue your journey, try running additional features with calc_cli.py, experiment with new feature configurations in app/configs/config.yaml, or dive into Tiferet’s documentation for advanced DDD techniques. The beauty of Tiferet lies in its ability to transform complexity into clarity—may your creations reflect this harmony.
|
@@ -5,11 +5,11 @@ except:
|
|
5
5
|
|
6
6
|
config = {
|
7
7
|
'description': 'A multi-purpose application framework embodying beauty in form.',
|
8
|
-
'author': 'Andrew Shatz',
|
8
|
+
'author': 'Andrew Shatz, Great Strength Systems',
|
9
9
|
'url': r'https://github.com/greatstrength/app',
|
10
10
|
'download_url': r'https://github.com/greatstrength/app',
|
11
11
|
'author_email': 'andrew@greatstrength.me',
|
12
|
-
'version': '1.0.0-
|
12
|
+
'version': '1.0.0-beta.0',
|
13
13
|
'license': 'BSD 3',
|
14
14
|
'install_requires': [
|
15
15
|
'schematics>=2.1.1',
|
@@ -22,9 +22,12 @@ config = {
|
|
22
22
|
'tiferet.commands',
|
23
23
|
'tiferet.configs',
|
24
24
|
'tiferet.contexts',
|
25
|
+
'tiferet.contracts',
|
25
26
|
'tiferet.data',
|
26
|
-
'tiferet.
|
27
|
-
'tiferet.
|
27
|
+
'tiferet.handlers',
|
28
|
+
'tiferet.models',
|
29
|
+
'tiferet.proxies',
|
30
|
+
'tiferet.proxies.yaml',
|
28
31
|
],
|
29
32
|
'scripts': [],
|
30
33
|
'name': 'tiferet',
|