django-rule-engine 1.0.0__py3-none-any.whl

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,5 @@
1
+ """Django rule-engine package."""
2
+
3
+ default_app_config = 'django_rule_engine.apps.DjangoRuleEngineConfig'
4
+
5
+ __version__ = '1.0.0'
@@ -0,0 +1,5 @@
1
+ """Base API module."""
2
+
3
+ from .views import validate_rule
4
+
5
+ __all__ = ['validate_rule']
@@ -0,0 +1,8 @@
1
+ """API URLs for rule validation."""
2
+
3
+ from django.urls import path
4
+ from .views import validate_rule
5
+
6
+ urlpatterns = [
7
+ path('validate-rule/', validate_rule, name='validate_rule'),
8
+ ]
@@ -0,0 +1,85 @@
1
+ """API views for rule validation."""
2
+
3
+ import json
4
+ from django.http import JsonResponse
5
+ from django.views.decorators.http import require_http_methods
6
+ from django.views.decorators.csrf import ensure_csrf_cookie
7
+ import rule_engine
8
+
9
+
10
+ @require_http_methods(["POST"])
11
+ @ensure_csrf_cookie
12
+ def validate_rule(request):
13
+ """
14
+ Valida uma regra do rule-engine com dados de exemplo.
15
+
16
+ POST /api/validate-rule/
17
+ Body: {
18
+ "rule": "age >= 18 and status == 'active'",
19
+ "data": {"age": 25, "status": "active"}
20
+ }
21
+
22
+ Response: {
23
+ "valid": true,
24
+ "result": true,
25
+ "matches": true
26
+ }
27
+
28
+ Ou em caso de erro:
29
+ {
30
+ "valid": false,
31
+ "error": "mensagem de erro"
32
+ }
33
+ """
34
+ print(f"DEBUG: validate_rule chamado - Method: {request.method}")
35
+ print(f"DEBUG: Path: {request.path}")
36
+ print(f"DEBUG: Body: {request.body}")
37
+
38
+ try:
39
+ # Parse request body
40
+ body = json.loads(request.body)
41
+ rule_text = body.get('rule', '')
42
+ data = body.get('data', {})
43
+
44
+ if not rule_text:
45
+ return JsonResponse({
46
+ 'valid': False,
47
+ 'error': 'Regra não pode ser vazia'
48
+ }, status=400)
49
+
50
+ # Compila a regra
51
+ try:
52
+ rule = rule_engine.Rule(rule_text)
53
+ except Exception as e:
54
+ return JsonResponse({
55
+ 'valid': False,
56
+ 'error': f'Erro ao compilar regra: {str(e)}'
57
+ }, status=400)
58
+
59
+ # Avalia a regra com os dados fornecidos
60
+ try:
61
+ result = rule.matches(data)
62
+
63
+ return JsonResponse({
64
+ 'valid': True,
65
+ 'result': result,
66
+ 'matches': result,
67
+ 'rule': rule_text,
68
+ 'data': data
69
+ })
70
+ except Exception as e:
71
+ return JsonResponse({
72
+ 'valid': False,
73
+ 'error': f'Erro ao avaliar regra: {str(e)}'
74
+ }, status=400)
75
+
76
+ except json.JSONDecodeError as e:
77
+ return JsonResponse({
78
+ 'valid': False,
79
+ 'error': f'JSON inválido: {str(e)}'
80
+ }, status=400)
81
+ except Exception as e:
82
+ return JsonResponse({
83
+ 'valid': False,
84
+ 'error': f'Erro interno: {str(e)}'
85
+ }, status=500)
@@ -0,0 +1,11 @@
1
+ """Django app configuration for django_rule_engine."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class DjangoRuleEngineConfig(AppConfig):
7
+ """Configuration for Django rule-engine app."""
8
+
9
+ default_auto_field = 'django.db.models.BigAutoField'
10
+ name = 'django_rule_engine'
11
+ verbose_name = 'Django Rule Engine'
@@ -0,0 +1,6 @@
1
+ """Custom Django fields for the application."""
2
+
3
+ from .rule_field import RuleField
4
+ from .rule_widget import RuleWidget
5
+
6
+ __all__ = ["RuleField", "RuleWidget"]
@@ -0,0 +1,89 @@
1
+ """Django field for rule-engine rules."""
2
+
3
+ import json
4
+ from django.db import models
5
+ from django.core.exceptions import ValidationError
6
+ from django.utils.translation import gettext_lazy as _
7
+ import rule_engine
8
+
9
+ from .rule_widget import RuleWidget
10
+
11
+
12
+ class RuleField(models.TextField):
13
+ """
14
+ Campo Django para armazenar e validar regras do rule-engine.
15
+
16
+ Args:
17
+ example_data: Dicionário ou string JSON com dados de exemplo para validação.
18
+ Pode ser sobrescrito pelo usuário no admin.
19
+ resolver: Opcional. Resolver customizado para rule-engine.
20
+ **kwargs: Argumentos adicionais do TextField.
21
+
22
+ Example:
23
+ class MyModel(models.Model):
24
+ rule = RuleField(
25
+ verbose_name="Regra de validação",
26
+ example_data={"age": 25, "status": "active"},
27
+ help_text="Digite uma regra válida do rule-engine"
28
+ )
29
+ """
30
+
31
+ description = _("Campo para regras do rule-engine")
32
+
33
+ def __init__(self, *args, example_data=None, resolver=None, **kwargs):
34
+ self.example_data = example_data
35
+ self.resolver = resolver
36
+ super().__init__(*args, **kwargs)
37
+
38
+ def deconstruct(self):
39
+ name, path, args, kwargs = super().deconstruct()
40
+ if self.example_data is not None:
41
+ kwargs['example_data'] = self.example_data
42
+ if self.resolver is not None:
43
+ kwargs['resolver'] = self.resolver
44
+ return name, path, args, kwargs
45
+
46
+ def formfield(self, **kwargs):
47
+ """Retorna o formfield com o widget customizado."""
48
+ # Prepara example_data para o widget
49
+ example_data_json = None
50
+ if self.example_data is not None:
51
+ if isinstance(self.example_data, str):
52
+ example_data_json = self.example_data
53
+ else:
54
+ example_data_json = json.dumps(self.example_data, indent=2)
55
+
56
+ # Define o widget como padrão
57
+ kwargs['widget'] = RuleWidget(example_data=example_data_json)
58
+
59
+ return super().formfield(**kwargs)
60
+
61
+ def validate(self, value, model_instance):
62
+ """Valida se a regra é válida."""
63
+ super().validate(value, model_instance)
64
+
65
+ if not value:
66
+ return
67
+
68
+ try:
69
+ # Tenta compilar a regra
70
+ # O resolver deve ser passado no contexto, não no init do Rule
71
+ rule_engine.Rule(value)
72
+ except Exception as e:
73
+ raise ValidationError(
74
+ _('Regra inválida: %(error)s'),
75
+ code='invalid_rule',
76
+ params={'error': str(e)},
77
+ )
78
+
79
+ def to_python(self, value):
80
+ """Converte o valor do banco para Python."""
81
+ if value is None:
82
+ return value
83
+ return str(value)
84
+
85
+ def get_prep_value(self, value):
86
+ """Prepara o valor para salvar no banco."""
87
+ if value is None:
88
+ return value
89
+ return str(value)
@@ -0,0 +1,85 @@
1
+ """Widget Django para edição de regras do rule-engine."""
2
+
3
+ import json
4
+ from django import forms
5
+ from django.utils.safestring import mark_safe
6
+ from django.conf import settings
7
+ from django_json_widget.widgets import JSONEditorWidget
8
+
9
+
10
+ class RuleWidget(forms.Textarea):
11
+ """
12
+ Widget customizado para editar regras do rule-engine.
13
+
14
+ Fornece:
15
+ - Editor de código com syntax highlighting (CodeMirror)
16
+ - Campo para JSON de exemplo (editável pelo usuário)
17
+ - Validação dinâmica no frontend
18
+ - Feedback visual do resultado da regra
19
+ """
20
+
21
+ template_name = 'widgets/rule_widget.html'
22
+
23
+ class Media:
24
+ css = {
25
+ 'all': (
26
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css',
27
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material.min.css',
28
+ 'rule_widget/rule_widget.css',
29
+ 'rule_widget/rule_widget_highlighting.css', # Estilos customizados de highlighting
30
+ )
31
+ }
32
+ js = (
33
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js',
34
+ 'rule_widget/codemirror-rule-engine.js', # Modo customizado para rule-engine
35
+ 'rule_widget/rule_widget.js',
36
+ )
37
+
38
+ def __init__(self, example_data=None, attrs=None):
39
+ """
40
+ Inicializa o widget.
41
+
42
+ Args:
43
+ example_data: JSON string com dados de exemplo
44
+ attrs: Atributos HTML adicionais
45
+ """
46
+ self.example_data = example_data or '{}'
47
+ super().__init__(attrs)
48
+
49
+ def get_context(self, name, value, attrs):
50
+ """Adiciona contexto extra para o template."""
51
+ context = super().get_context(name, value, attrs)
52
+
53
+ # Formata o example_data para exibição
54
+ try:
55
+ if isinstance(self.example_data, str):
56
+ example_obj = json.loads(self.example_data)
57
+ else:
58
+ example_obj = self.example_data
59
+ formatted_example = json.dumps(example_obj, indent=2)
60
+ except (json.JSONDecodeError, TypeError):
61
+ formatted_example = self.example_data
62
+
63
+ # Cria um JSONEditorWidget para o campo de exemplo
64
+ json_widget = JSONEditorWidget(height='120px') # ~5 linhas
65
+ json_widget_name = f"{name}_example_data"
66
+ json_widget_attrs = {
67
+ 'id': f"{attrs.get('id', name)}_example_data",
68
+ 'class': 'example-data-json-editor',
69
+ }
70
+ json_widget_html = json_widget.render(json_widget_name, formatted_example, json_widget_attrs)
71
+
72
+ context['widget'].update({
73
+ 'example_data': formatted_example,
74
+ 'field_id': attrs.get('id', name),
75
+ 'json_widget_html': mark_safe(json_widget_html),
76
+ 'json_widget_media': json_widget.media,
77
+ })
78
+
79
+ return context
80
+
81
+ def format_value(self, value):
82
+ """Formata o valor para exibição no widget."""
83
+ if value is None:
84
+ return ''
85
+ return str(value)
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Script de teste do RuleField.
4
+
5
+ Execute com:
6
+ python manage.py shell < base/fields/test_rule_field.py
7
+
8
+ Ou no shell do Django:
9
+ from django_rule_engine.fields.test_rule_field import test_all
10
+ test_all()
11
+ """
12
+
13
+ import json
14
+
15
+
16
+ def test_rule_compilation():
17
+ """Testa compilação básica de regras."""
18
+ import rule_engine
19
+
20
+ print("=" * 60)
21
+ print("TESTE 1: Compilação de Regras")
22
+ print("=" * 60)
23
+
24
+ regras = [
25
+ "age >= 18",
26
+ 'status == "active"',
27
+ "age >= 18 and status == 'active'",
28
+ '(price > 100 or quantity >= 5) and customer_type == "premium"',
29
+ '"@ifrn.edu.br" in email',
30
+ "len(name) > 3",
31
+ ]
32
+
33
+ for regra in regras:
34
+ try:
35
+ rule = rule_engine.Rule(regra)
36
+ print(f"✓ OK: {regra}")
37
+ except Exception as e:
38
+ print(f"✗ ERRO: {regra}")
39
+ print(f" Mensagem: {e}")
40
+
41
+ print()
42
+
43
+
44
+ def test_rule_evaluation():
45
+ """Testa avaliação de regras com dados."""
46
+ import rule_engine
47
+
48
+ print("=" * 60)
49
+ print("TESTE 2: Avaliação de Regras")
50
+ print("=" * 60)
51
+
52
+ casos = [
53
+ {
54
+ "regra": "age >= 18",
55
+ "dados": {"age": 25},
56
+ "esperado": True
57
+ },
58
+ {
59
+ "regra": "age >= 18",
60
+ "dados": {"age": 15},
61
+ "esperado": False
62
+ },
63
+ {
64
+ "regra": 'status == "active"',
65
+ "dados": {"status": "active"},
66
+ "esperado": True
67
+ },
68
+ {
69
+ "regra": 'age >= 18 and status == "active"',
70
+ "dados": {"age": 25, "status": "active"},
71
+ "esperado": True
72
+ },
73
+ {
74
+ "regra": 'age >= 18 and status == "active"',
75
+ "dados": {"age": 15, "status": "active"},
76
+ "esperado": False
77
+ },
78
+ {
79
+ "regra": '"@ifrn.edu.br" in email',
80
+ "dados": {"email": "usuario@ifrn.edu.br"},
81
+ "esperado": True
82
+ },
83
+ {
84
+ "regra": "len(name) > 3",
85
+ "dados": {"name": "João Silva"},
86
+ "esperado": True
87
+ },
88
+ {
89
+ "regra": "(price * quantity) > 1000",
90
+ "dados": {"price": 100, "quantity": 12},
91
+ "esperado": True
92
+ },
93
+ ]
94
+
95
+ for i, caso in enumerate(casos, 1):
96
+ regra = rule_engine.Rule(caso["regra"])
97
+ resultado = regra.matches(caso["dados"])
98
+
99
+ status = "✓" if resultado == caso["esperado"] else "✗"
100
+ print(f"{status} Caso {i}:")
101
+ print(f" Regra: {caso['regra']}")
102
+ print(f" Dados: {caso['dados']}")
103
+ print(f" Resultado: {resultado} (esperado: {caso['esperado']})")
104
+ print()
105
+
106
+
107
+ def test_field_validation():
108
+ """Testa validação do campo Django."""
109
+ from django_rule_engine.fields import RuleField
110
+ from django.core.exceptions import ValidationError
111
+
112
+ print("=" * 60)
113
+ print("TESTE 3: Validação do Campo Django")
114
+ print("=" * 60)
115
+
116
+ field = RuleField()
117
+
118
+ # Regras válidas
119
+ regras_validas = [
120
+ "age >= 18",
121
+ 'status == "active"',
122
+ "age >= 18 and status == 'active'",
123
+ ]
124
+
125
+ print("Testando regras VÁLIDAS:")
126
+ for regra in regras_validas:
127
+ try:
128
+ field.validate(regra, None)
129
+ print(f"✓ OK: {regra}")
130
+ except ValidationError as e:
131
+ print(f"✗ ERRO (inesperado): {regra}")
132
+ print(f" Mensagem: {e}")
133
+
134
+ print()
135
+
136
+ # Regras inválidas
137
+ regras_invalidas = [
138
+ "age >>= 18", # Operador inválido
139
+ "age == ", # Incompleta
140
+ "status = 'active'", # Usa = ao invés de ==
141
+ ]
142
+
143
+ print("Testando regras INVÁLIDAS:")
144
+ for regra in regras_invalidas:
145
+ try:
146
+ field.validate(regra, None)
147
+ print(f"✗ ERRO (deveria falhar): {regra}")
148
+ except ValidationError as e:
149
+ print(f"✓ OK (erro esperado): {regra}")
150
+ print(f" Mensagem: {e}")
151
+
152
+ print()
153
+
154
+
155
+ def test_api_simulation():
156
+ """Simula chamada à API de validação."""
157
+ import rule_engine
158
+
159
+ print("=" * 60)
160
+ print("TESTE 4: Simulação da API")
161
+ print("=" * 60)
162
+
163
+ request_body = {
164
+ "rule": "age >= 18 and status == 'active'",
165
+ "data": {
166
+ "age": 25,
167
+ "status": "active"
168
+ }
169
+ }
170
+
171
+ print(f"Request Body:")
172
+ print(json.dumps(request_body, indent=2))
173
+ print()
174
+
175
+ try:
176
+ rule = rule_engine.Rule(request_body["rule"])
177
+ result = rule.matches(request_body["data"])
178
+
179
+ response = {
180
+ "valid": True,
181
+ "result": result,
182
+ "matches": result,
183
+ "rule": request_body["rule"],
184
+ "data": request_body["data"]
185
+ }
186
+
187
+ print("Response:")
188
+ print(json.dumps(response, indent=2))
189
+ print()
190
+ print("✓ API simulada com sucesso!")
191
+
192
+ except Exception as e:
193
+ response = {
194
+ "valid": False,
195
+ "error": str(e)
196
+ }
197
+ print("Response (ERRO):")
198
+ print(json.dumps(response, indent=2))
199
+ print()
200
+ print("✗ Erro na simulação da API")
201
+
202
+ print()
203
+
204
+
205
+ def test_real_world_examples():
206
+ """Testa exemplos do mundo real."""
207
+ import rule_engine
208
+
209
+ print("=" * 60)
210
+ print("TESTE 5: Exemplos do Mundo Real")
211
+ print("=" * 60)
212
+
213
+ exemplos = [
214
+ {
215
+ "nome": "Validação de Matrícula",
216
+ "regra": 'idade >= 16 and nivel_ensino == "medio" and aprovado == true',
217
+ "dados": {
218
+ "idade": 18,
219
+ "nivel_ensino": "medio",
220
+ "aprovado": True
221
+ },
222
+ "esperado": True
223
+ },
224
+ {
225
+ "nome": "Desconto para Cliente Premium",
226
+ "regra": '(quantidade >= 10 or valor_total > 1000) and tipo_cliente == "premium"',
227
+ "dados": {
228
+ "quantidade": 12,
229
+ "valor_total": 500,
230
+ "tipo_cliente": "premium"
231
+ },
232
+ "esperado": True
233
+ },
234
+ {
235
+ "nome": "Email Institucional",
236
+ "regra": '"@ifrn.edu.br" in email and ativo == true',
237
+ "dados": {
238
+ "email": "joao.silva@ifrn.edu.br",
239
+ "ativo": True
240
+ },
241
+ "esperado": True
242
+ },
243
+ {
244
+ "nome": "Acesso de Administrador",
245
+ "regra": 'is_admin == true or (departamento == "TI" and nivel_acesso >= 5)',
246
+ "dados": {
247
+ "is_admin": False,
248
+ "departamento": "TI",
249
+ "nivel_acesso": 7
250
+ },
251
+ "esperado": True
252
+ },
253
+ ]
254
+
255
+ for exemplo in exemplos:
256
+ print(f"\n{exemplo['nome']}:")
257
+ print(f" Regra: {exemplo['regra']}")
258
+ print(f" Dados: {json.dumps(exemplo['dados'], indent=4)}")
259
+
260
+ try:
261
+ rule = rule_engine.Rule(exemplo["regra"])
262
+ resultado = rule.matches(exemplo["dados"])
263
+
264
+ status = "✓" if resultado == exemplo["esperado"] else "✗"
265
+ print(f" {status} Resultado: {resultado} (esperado: {exemplo['esperado']})")
266
+
267
+ except Exception as e:
268
+ print(f" ✗ ERRO: {e}")
269
+
270
+ print()
271
+
272
+
273
+ def test_all():
274
+ """Executa todos os testes."""
275
+ print("\n" + "=" * 60)
276
+ print("INICIANDO TESTES DO RULEFIELD")
277
+ print("=" * 60 + "\n")
278
+
279
+ try:
280
+ test_rule_compilation()
281
+ test_rule_evaluation()
282
+ test_field_validation()
283
+ test_api_simulation()
284
+ test_real_world_examples()
285
+
286
+ print("=" * 60)
287
+ print("TODOS OS TESTES CONCLUÍDOS!")
288
+ print("=" * 60)
289
+
290
+ except Exception as e:
291
+ print(f"\n✗ ERRO DURANTE OS TESTES: {e}")
292
+ import traceback
293
+ traceback.print_exc()
294
+
295
+
296
+ if __name__ == "__main__":
297
+ test_all()