constec 0.4.0__tar.gz → 0.5.0__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.
- {constec-0.4.0 → constec-0.5.0}/PKG-INFO +1 -1
- constec-0.5.0/constec/db/migrations/0005_event.py +46 -0
- constec-0.5.0/constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py +270 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/__init__.py +10 -0
- constec-0.5.0/constec/db/models/automation.py +467 -0
- {constec-0.4.0 → constec-0.5.0}/constec.egg-info/PKG-INFO +1 -1
- {constec-0.4.0 → constec-0.5.0}/constec.egg-info/SOURCES.txt +3 -0
- {constec-0.4.0 → constec-0.5.0}/pyproject.toml +1 -1
- {constec-0.4.0 → constec-0.5.0}/LICENSE +0 -0
- {constec-0.4.0 → constec-0.5.0}/README.md +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/__init__.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/apps.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/migrations/0001_initial.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/migrations/0002_module_level.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/migrations/0003_remove_module_level.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/migrations/__init__.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/base.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/company.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/contact.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/erp.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/erp_entity.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/flow.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/group.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/module.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/organization.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/person.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/session.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/tag.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/db/models/user.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/py.typed +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/services/__init__.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/services/encryption.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/shared/__init__.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/shared/exceptions.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/utils/__init__.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/utils/cuit.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec/utils/password.py +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec.egg-info/dependency_links.txt +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec.egg-info/requires.txt +0 -0
- {constec-0.4.0 → constec-0.5.0}/constec.egg-info/top_level.txt +0 -0
- {constec-0.4.0 → constec-0.5.0}/setup.cfg +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Generated manually on 2026-02-02 for automations schema
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('constec_db', '0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='Event',
|
|
18
|
+
fields=[
|
|
19
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
20
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
21
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
22
|
+
('event_type', models.CharField(choices=[('notification', 'Notification'), ('automation', 'Automation')], max_length=50)),
|
|
23
|
+
('title', models.CharField(max_length=255)),
|
|
24
|
+
('description', models.TextField(blank=True)),
|
|
25
|
+
('status', models.CharField(choices=[('active', 'Active'), ('paused', 'Paused'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='active', max_length=20)),
|
|
26
|
+
('recurrence_type', models.CharField(choices=[('punctual', 'Punctual'), ('periodic', 'Periodic')], help_text='Tipo de recurrencia: puntual (fechas específicas) o periódico', max_length=20)),
|
|
27
|
+
('recurrence_config', models.JSONField(help_text='Configuración de recurrencia en formato JSON')),
|
|
28
|
+
('config', models.JSONField(default=dict, help_text='Configuración específica según event_type (mensaje, URL, etc.)')),
|
|
29
|
+
('next_execution_at', models.DateTimeField(blank=True, db_index=True, help_text='Próxima fecha/hora de ejecución calculada', null=True)),
|
|
30
|
+
('last_executed_at', models.DateTimeField(blank=True, help_text='Última vez que se ejecutó', null=True)),
|
|
31
|
+
('execution_count', models.IntegerField(default=0, help_text='Cantidad de veces que se ha ejecutado')),
|
|
32
|
+
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='constec_db.company')),
|
|
33
|
+
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL)),
|
|
34
|
+
],
|
|
35
|
+
options={
|
|
36
|
+
'db_table': '"automations"."events"',
|
|
37
|
+
'ordering': ['next_execution_at'],
|
|
38
|
+
'indexes': [
|
|
39
|
+
models.Index(fields=['company', 'event_type'], name='automations_company_ebe85a_idx'),
|
|
40
|
+
models.Index(fields=['status', 'next_execution_at'], name='automations_status_a3d45f_idx'),
|
|
41
|
+
models.Index(fields=['recurrence_type'], name='automations_recurre_5d8b2c_idx'),
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
bases=(models.Model,),
|
|
45
|
+
),
|
|
46
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Generated manually on 2026-02-03 for automations redesign (Fase 1)
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('constec_db', '0005_event'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
# Create Automation model
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='Automation',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
21
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
22
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
23
|
+
('name', models.CharField(max_length=255)),
|
|
24
|
+
('description', models.TextField(blank=True)),
|
|
25
|
+
('status', models.CharField(
|
|
26
|
+
choices=[('active', 'Active'), ('paused', 'Paused'), ('archived', 'Archived'), ('draft', 'Draft')],
|
|
27
|
+
default='draft',
|
|
28
|
+
max_length=20
|
|
29
|
+
)),
|
|
30
|
+
('execution_mode', models.CharField(
|
|
31
|
+
choices=[('sequential', 'Sequential'), ('parallel', 'Parallel')],
|
|
32
|
+
default='sequential',
|
|
33
|
+
max_length=20
|
|
34
|
+
)),
|
|
35
|
+
('max_executions', models.IntegerField(
|
|
36
|
+
blank=True,
|
|
37
|
+
help_text='Máximo número de ejecuciones (null = ilimitado)',
|
|
38
|
+
null=True
|
|
39
|
+
)),
|
|
40
|
+
('execution_count', models.IntegerField(default=0)),
|
|
41
|
+
('on_error', models.CharField(
|
|
42
|
+
choices=[('stop', 'Stop'), ('continue', 'Continue'), ('retry', 'Retry')],
|
|
43
|
+
default='stop',
|
|
44
|
+
max_length=20
|
|
45
|
+
)),
|
|
46
|
+
('retry_count', models.IntegerField(default=3)),
|
|
47
|
+
('retry_delay_seconds', models.IntegerField(default=60)),
|
|
48
|
+
('last_executed_at', models.DateTimeField(blank=True, null=True)),
|
|
49
|
+
('next_execution_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
|
50
|
+
('tags', models.JSONField(blank=True, default=list)),
|
|
51
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
|
52
|
+
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='constec_db.company')),
|
|
53
|
+
('created_by', models.ForeignKey(
|
|
54
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
55
|
+
related_name='created_automations',
|
|
56
|
+
to=settings.AUTH_USER_MODEL
|
|
57
|
+
)),
|
|
58
|
+
],
|
|
59
|
+
options={
|
|
60
|
+
'db_table': '"automations"."automations"',
|
|
61
|
+
'ordering': ['-created_at'],
|
|
62
|
+
'indexes': [
|
|
63
|
+
models.Index(fields=['company', 'status'], name='automations_auto_company_status_idx'),
|
|
64
|
+
models.Index(fields=['status', 'next_execution_at'], name='automations_auto_status_next_idx'),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
bases=(models.Model,),
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
# Create Trigger model
|
|
71
|
+
migrations.CreateModel(
|
|
72
|
+
name='Trigger',
|
|
73
|
+
fields=[
|
|
74
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
75
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
76
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
77
|
+
('trigger_type', models.CharField(
|
|
78
|
+
choices=[('schedule', 'Schedule'), ('webhook', 'Webhook'), ('manual', 'Manual'), ('event', 'Event')],
|
|
79
|
+
max_length=20
|
|
80
|
+
)),
|
|
81
|
+
('name', models.CharField(max_length=255)),
|
|
82
|
+
('description', models.TextField(blank=True)),
|
|
83
|
+
('is_enabled', models.BooleanField(default=True)),
|
|
84
|
+
('config', models.JSONField(
|
|
85
|
+
default=dict,
|
|
86
|
+
help_text='Configuración específica del tipo de trigger (schedule, webhook, etc.)'
|
|
87
|
+
)),
|
|
88
|
+
('last_triggered_at', models.DateTimeField(blank=True, null=True)),
|
|
89
|
+
('trigger_count', models.IntegerField(default=0)),
|
|
90
|
+
('priority', models.IntegerField(default=50, help_text='0-100, mayor = más prioridad')),
|
|
91
|
+
('automation', models.ForeignKey(
|
|
92
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
93
|
+
related_name='triggers',
|
|
94
|
+
to='constec_db.automation'
|
|
95
|
+
)),
|
|
96
|
+
],
|
|
97
|
+
options={
|
|
98
|
+
'db_table': '"automations"."triggers"',
|
|
99
|
+
'ordering': ['-priority', 'created_at'],
|
|
100
|
+
'indexes': [
|
|
101
|
+
models.Index(fields=['automation', 'is_enabled'], name='automations_trig_automation_enabled_idx'),
|
|
102
|
+
models.Index(fields=['trigger_type', 'is_enabled'], name='automations_trig_type_enabled_idx'),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
bases=(models.Model,),
|
|
106
|
+
),
|
|
107
|
+
|
|
108
|
+
# Create Action model
|
|
109
|
+
migrations.CreateModel(
|
|
110
|
+
name='Action',
|
|
111
|
+
fields=[
|
|
112
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
113
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
114
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
115
|
+
('action_type', models.CharField(
|
|
116
|
+
choices=[('notification', 'Notification'), ('webhook', 'Webhook'), ('batch_task', 'Batch Task'), ('flow_execution', 'Flow Execution')],
|
|
117
|
+
max_length=20
|
|
118
|
+
)),
|
|
119
|
+
('name', models.CharField(max_length=255)),
|
|
120
|
+
('description', models.TextField(blank=True)),
|
|
121
|
+
('is_enabled', models.BooleanField(default=True)),
|
|
122
|
+
('order', models.IntegerField(default=0, help_text='Orden de ejecución')),
|
|
123
|
+
('config', models.JSONField(
|
|
124
|
+
default=dict,
|
|
125
|
+
help_text='Configuración específica del tipo de acción'
|
|
126
|
+
)),
|
|
127
|
+
('on_error', models.CharField(
|
|
128
|
+
blank=True,
|
|
129
|
+
choices=[('stop', 'Stop'), ('continue', 'Continue'), ('retry', 'Retry')],
|
|
130
|
+
help_text='Override del on_error de la automation',
|
|
131
|
+
max_length=20,
|
|
132
|
+
null=True
|
|
133
|
+
)),
|
|
134
|
+
('retry_count', models.IntegerField(
|
|
135
|
+
blank=True,
|
|
136
|
+
help_text='Override del retry_count de la automation',
|
|
137
|
+
null=True
|
|
138
|
+
)),
|
|
139
|
+
('condition', models.JSONField(
|
|
140
|
+
blank=True,
|
|
141
|
+
help_text='Condición JSONLogic para ejecutar esta acción',
|
|
142
|
+
null=True
|
|
143
|
+
)),
|
|
144
|
+
('last_executed_at', models.DateTimeField(blank=True, null=True)),
|
|
145
|
+
('execution_count', models.IntegerField(default=0)),
|
|
146
|
+
('success_count', models.IntegerField(default=0)),
|
|
147
|
+
('failure_count', models.IntegerField(default=0)),
|
|
148
|
+
('automation', models.ForeignKey(
|
|
149
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
150
|
+
related_name='actions',
|
|
151
|
+
to='constec_db.automation'
|
|
152
|
+
)),
|
|
153
|
+
],
|
|
154
|
+
options={
|
|
155
|
+
'db_table': '"automations"."actions"',
|
|
156
|
+
'ordering': ['order', 'created_at'],
|
|
157
|
+
'indexes': [
|
|
158
|
+
models.Index(fields=['automation', 'order'], name='automations_act_automation_order_idx'),
|
|
159
|
+
models.Index(fields=['action_type', 'is_enabled'], name='automations_act_type_enabled_idx'),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
bases=(models.Model,),
|
|
163
|
+
),
|
|
164
|
+
|
|
165
|
+
# Create ExecutionLog model
|
|
166
|
+
migrations.CreateModel(
|
|
167
|
+
name='ExecutionLog',
|
|
168
|
+
fields=[
|
|
169
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
170
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
171
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
172
|
+
('status', models.CharField(
|
|
173
|
+
choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('partial', 'Partial')],
|
|
174
|
+
default='pending',
|
|
175
|
+
max_length=20
|
|
176
|
+
)),
|
|
177
|
+
('triggered_by', models.CharField(
|
|
178
|
+
choices=[('schedule', 'Schedule'), ('webhook', 'Webhook'), ('manual', 'Manual'), ('system', 'System')],
|
|
179
|
+
max_length=20
|
|
180
|
+
)),
|
|
181
|
+
('started_at', models.DateTimeField(auto_now_add=True)),
|
|
182
|
+
('completed_at', models.DateTimeField(blank=True, null=True)),
|
|
183
|
+
('duration_seconds', models.IntegerField(blank=True, null=True)),
|
|
184
|
+
('execution_context', models.JSONField(
|
|
185
|
+
default=dict,
|
|
186
|
+
help_text='Variables y contexto disponible durante la ejecución'
|
|
187
|
+
)),
|
|
188
|
+
('actions_total', models.IntegerField(default=0)),
|
|
189
|
+
('actions_succeeded', models.IntegerField(default=0)),
|
|
190
|
+
('actions_failed', models.IntegerField(default=0)),
|
|
191
|
+
('actions_skipped', models.IntegerField(default=0)),
|
|
192
|
+
('action_logs', models.JSONField(
|
|
193
|
+
default=list,
|
|
194
|
+
help_text='Array de logs detallados de cada acción ejecutada'
|
|
195
|
+
)),
|
|
196
|
+
('error_message', models.TextField(blank=True)),
|
|
197
|
+
('error_traceback', models.TextField(blank=True)),
|
|
198
|
+
('automation', models.ForeignKey(
|
|
199
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
200
|
+
related_name='execution_logs',
|
|
201
|
+
to='constec_db.automation'
|
|
202
|
+
)),
|
|
203
|
+
('trigger', models.ForeignKey(
|
|
204
|
+
blank=True,
|
|
205
|
+
null=True,
|
|
206
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
207
|
+
related_name='execution_logs',
|
|
208
|
+
to='constec_db.trigger'
|
|
209
|
+
)),
|
|
210
|
+
('triggered_by_user', models.ForeignKey(
|
|
211
|
+
blank=True,
|
|
212
|
+
null=True,
|
|
213
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
214
|
+
related_name='triggered_executions',
|
|
215
|
+
to=settings.AUTH_USER_MODEL
|
|
216
|
+
)),
|
|
217
|
+
],
|
|
218
|
+
options={
|
|
219
|
+
'db_table': '"automations"."execution_logs"',
|
|
220
|
+
'ordering': ['-started_at'],
|
|
221
|
+
'indexes': [
|
|
222
|
+
models.Index(fields=['automation', 'started_at'], name='automations_exec_automation_started_idx'),
|
|
223
|
+
models.Index(fields=['status', 'started_at'], name='automations_exec_status_started_idx'),
|
|
224
|
+
models.Index(fields=['triggered_by', 'started_at'], name='automations_exec_triggered_started_idx'),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
bases=(models.Model,),
|
|
228
|
+
),
|
|
229
|
+
|
|
230
|
+
# Create NotificationTemplate model
|
|
231
|
+
migrations.CreateModel(
|
|
232
|
+
name='NotificationTemplate',
|
|
233
|
+
fields=[
|
|
234
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
235
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
236
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
237
|
+
('name', models.CharField(max_length=255)),
|
|
238
|
+
('description', models.TextField(blank=True)),
|
|
239
|
+
('channel', models.CharField(
|
|
240
|
+
choices=[('email', 'Email'), ('whatsapp', 'WhatsApp'), ('sms', 'SMS'), ('push', 'Push Notification')],
|
|
241
|
+
max_length=20
|
|
242
|
+
)),
|
|
243
|
+
('subject', models.CharField(
|
|
244
|
+
blank=True,
|
|
245
|
+
help_text='Asunto (solo para email)',
|
|
246
|
+
max_length=255
|
|
247
|
+
)),
|
|
248
|
+
('body', models.TextField(help_text='Cuerpo del mensaje con variables {{variable}}')),
|
|
249
|
+
('variables', models.JSONField(
|
|
250
|
+
default=list,
|
|
251
|
+
help_text="Array de nombres de variables esperadas ['customer_name', 'amount']"
|
|
252
|
+
)),
|
|
253
|
+
('is_active', models.BooleanField(default=True)),
|
|
254
|
+
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='constec_db.company')),
|
|
255
|
+
('created_by', models.ForeignKey(
|
|
256
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
257
|
+
related_name='created_templates',
|
|
258
|
+
to=settings.AUTH_USER_MODEL
|
|
259
|
+
)),
|
|
260
|
+
],
|
|
261
|
+
options={
|
|
262
|
+
'db_table': '"automations"."notification_templates"',
|
|
263
|
+
'ordering': ['name'],
|
|
264
|
+
'indexes': [
|
|
265
|
+
models.Index(fields=['company', 'channel', 'is_active'], name='automations_tmpl_company_channel_idx'),
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
bases=(models.Model,),
|
|
269
|
+
),
|
|
270
|
+
]
|
|
@@ -41,6 +41,9 @@ ErpEntity = Entity
|
|
|
41
41
|
from .flow import FlowTemplate, Flow
|
|
42
42
|
from .session import Session, Message
|
|
43
43
|
|
|
44
|
+
# Automations models (automations schema)
|
|
45
|
+
from .automation import Event, Automation, Trigger, Action, ExecutionLog, NotificationTemplate
|
|
46
|
+
|
|
44
47
|
|
|
45
48
|
__all__ = [
|
|
46
49
|
# Base
|
|
@@ -89,4 +92,11 @@ __all__ = [
|
|
|
89
92
|
'Flow',
|
|
90
93
|
'Session',
|
|
91
94
|
'Message',
|
|
95
|
+
# Automations (automations schema)
|
|
96
|
+
'Event',
|
|
97
|
+
'Automation',
|
|
98
|
+
'Trigger',
|
|
99
|
+
'Action',
|
|
100
|
+
'ExecutionLog',
|
|
101
|
+
'NotificationTemplate',
|
|
92
102
|
]
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Automation models for event scheduling and task automation."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.core.exceptions import ValidationError
|
|
6
|
+
from .base import UUIDModel
|
|
7
|
+
from .company import Company
|
|
8
|
+
from .user import User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Automation(UUIDModel):
|
|
12
|
+
"""
|
|
13
|
+
Automatización principal basada en el patrón Trigger-Action.
|
|
14
|
+
|
|
15
|
+
Una automatización define un conjunto de acciones que se ejecutan cuando
|
|
16
|
+
se activan uno o más triggers (disparadores).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
STATUS_CHOICES = [
|
|
20
|
+
('active', 'Active'),
|
|
21
|
+
('paused', 'Paused'),
|
|
22
|
+
('archived', 'Archived'),
|
|
23
|
+
('draft', 'Draft'),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
EXECUTION_MODE_CHOICES = [
|
|
27
|
+
('sequential', 'Sequential'),
|
|
28
|
+
('parallel', 'Parallel'), # Futuro
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
ON_ERROR_CHOICES = [
|
|
32
|
+
('stop', 'Stop'),
|
|
33
|
+
('continue', 'Continue'),
|
|
34
|
+
('retry', 'Retry'),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# FKs
|
|
38
|
+
company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
|
|
39
|
+
created_by = models.ForeignKey(
|
|
40
|
+
User,
|
|
41
|
+
on_delete=models.CASCADE,
|
|
42
|
+
related_name='created_automations'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Info básica
|
|
46
|
+
name = models.CharField(max_length=255)
|
|
47
|
+
description = models.TextField(blank=True)
|
|
48
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
|
49
|
+
|
|
50
|
+
# Configuración de ejecución
|
|
51
|
+
execution_mode = models.CharField(
|
|
52
|
+
max_length=20,
|
|
53
|
+
choices=EXECUTION_MODE_CHOICES,
|
|
54
|
+
default='sequential'
|
|
55
|
+
)
|
|
56
|
+
max_executions = models.IntegerField(
|
|
57
|
+
null=True,
|
|
58
|
+
blank=True,
|
|
59
|
+
help_text="Máximo número de ejecuciones (null = ilimitado)"
|
|
60
|
+
)
|
|
61
|
+
execution_count = models.IntegerField(default=0)
|
|
62
|
+
|
|
63
|
+
# Manejo de errores
|
|
64
|
+
on_error = models.CharField(
|
|
65
|
+
max_length=20,
|
|
66
|
+
choices=ON_ERROR_CHOICES,
|
|
67
|
+
default='stop'
|
|
68
|
+
)
|
|
69
|
+
retry_count = models.IntegerField(default=3)
|
|
70
|
+
retry_delay_seconds = models.IntegerField(default=60)
|
|
71
|
+
|
|
72
|
+
# Tracking de ejecución
|
|
73
|
+
last_executed_at = models.DateTimeField(null=True, blank=True)
|
|
74
|
+
next_execution_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
|
75
|
+
|
|
76
|
+
# Metadata
|
|
77
|
+
tags = models.JSONField(default=list, blank=True)
|
|
78
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
79
|
+
|
|
80
|
+
class Meta:
|
|
81
|
+
db_table = '"automations"."automations"'
|
|
82
|
+
app_label = 'constec_db'
|
|
83
|
+
indexes = [
|
|
84
|
+
models.Index(fields=['company', 'status']),
|
|
85
|
+
models.Index(fields=['status', 'next_execution_at']),
|
|
86
|
+
]
|
|
87
|
+
ordering = ['-created_at']
|
|
88
|
+
|
|
89
|
+
def __str__(self):
|
|
90
|
+
return f"{self.name} ({self.status})"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Trigger(UUIDModel):
|
|
94
|
+
"""
|
|
95
|
+
Trigger (disparador) que inicia la ejecución de una automatización.
|
|
96
|
+
|
|
97
|
+
Tipos soportados:
|
|
98
|
+
- schedule: Programado por fecha/hora
|
|
99
|
+
- webhook: Disparado por HTTP POST
|
|
100
|
+
- manual: Ejecución manual del usuario
|
|
101
|
+
- event: Disparado por evento del sistema (futuro)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
TRIGGER_TYPE_CHOICES = [
|
|
105
|
+
('schedule', 'Schedule'),
|
|
106
|
+
('webhook', 'Webhook'),
|
|
107
|
+
('manual', 'Manual'),
|
|
108
|
+
('event', 'Event'),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# FKs
|
|
112
|
+
automation = models.ForeignKey(
|
|
113
|
+
Automation,
|
|
114
|
+
on_delete=models.CASCADE,
|
|
115
|
+
related_name='triggers'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Info básica
|
|
119
|
+
trigger_type = models.CharField(max_length=20, choices=TRIGGER_TYPE_CHOICES)
|
|
120
|
+
name = models.CharField(max_length=255)
|
|
121
|
+
description = models.TextField(blank=True)
|
|
122
|
+
is_enabled = models.BooleanField(default=True)
|
|
123
|
+
|
|
124
|
+
# Configuración tipo-específica
|
|
125
|
+
config = models.JSONField(
|
|
126
|
+
default=dict,
|
|
127
|
+
help_text="Configuración específica del tipo de trigger (schedule, webhook, etc.)"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Tracking
|
|
131
|
+
last_triggered_at = models.DateTimeField(null=True, blank=True)
|
|
132
|
+
trigger_count = models.IntegerField(default=0)
|
|
133
|
+
priority = models.IntegerField(default=50, help_text="0-100, mayor = más prioridad")
|
|
134
|
+
|
|
135
|
+
class Meta:
|
|
136
|
+
db_table = '"automations"."triggers"'
|
|
137
|
+
app_label = 'constec_db'
|
|
138
|
+
indexes = [
|
|
139
|
+
models.Index(fields=['automation', 'is_enabled']),
|
|
140
|
+
models.Index(fields=['trigger_type', 'is_enabled']),
|
|
141
|
+
]
|
|
142
|
+
ordering = ['-priority', 'created_at']
|
|
143
|
+
|
|
144
|
+
def __str__(self):
|
|
145
|
+
return f"{self.trigger_type}: {self.name}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Action(UUIDModel):
|
|
149
|
+
"""
|
|
150
|
+
Acción que se ejecuta como parte de una automatización.
|
|
151
|
+
|
|
152
|
+
Tipos soportados:
|
|
153
|
+
- notification: Enviar notificación (email, SMS, WhatsApp, push)
|
|
154
|
+
- webhook: Ejecutar HTTP request
|
|
155
|
+
- batch_task: Ejecutar tarea batch (futuro)
|
|
156
|
+
- flow_execution: Ejecutar workflow LangGraph (futuro)
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
ACTION_TYPE_CHOICES = [
|
|
160
|
+
('notification', 'Notification'),
|
|
161
|
+
('webhook', 'Webhook'),
|
|
162
|
+
('batch_task', 'Batch Task'),
|
|
163
|
+
('flow_execution', 'Flow Execution'),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
ON_ERROR_CHOICES = [
|
|
167
|
+
('stop', 'Stop'),
|
|
168
|
+
('continue', 'Continue'),
|
|
169
|
+
('retry', 'Retry'),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
# FKs
|
|
173
|
+
automation = models.ForeignKey(
|
|
174
|
+
Automation,
|
|
175
|
+
on_delete=models.CASCADE,
|
|
176
|
+
related_name='actions'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Info básica
|
|
180
|
+
action_type = models.CharField(max_length=20, choices=ACTION_TYPE_CHOICES)
|
|
181
|
+
name = models.CharField(max_length=255)
|
|
182
|
+
description = models.TextField(blank=True)
|
|
183
|
+
is_enabled = models.BooleanField(default=True)
|
|
184
|
+
order = models.IntegerField(default=0, help_text="Orden de ejecución")
|
|
185
|
+
|
|
186
|
+
# Configuración tipo-específica
|
|
187
|
+
config = models.JSONField(
|
|
188
|
+
default=dict,
|
|
189
|
+
help_text="Configuración específica del tipo de acción"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Manejo de errores (override de automation)
|
|
193
|
+
on_error = models.CharField(
|
|
194
|
+
max_length=20,
|
|
195
|
+
choices=ON_ERROR_CHOICES,
|
|
196
|
+
null=True,
|
|
197
|
+
blank=True,
|
|
198
|
+
help_text="Override del on_error de la automation"
|
|
199
|
+
)
|
|
200
|
+
retry_count = models.IntegerField(
|
|
201
|
+
null=True,
|
|
202
|
+
blank=True,
|
|
203
|
+
help_text="Override del retry_count de la automation"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Ejecución condicional (futuro)
|
|
207
|
+
condition = models.JSONField(
|
|
208
|
+
null=True,
|
|
209
|
+
blank=True,
|
|
210
|
+
help_text="Condición JSONLogic para ejecutar esta acción"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Tracking
|
|
214
|
+
last_executed_at = models.DateTimeField(null=True, blank=True)
|
|
215
|
+
execution_count = models.IntegerField(default=0)
|
|
216
|
+
success_count = models.IntegerField(default=0)
|
|
217
|
+
failure_count = models.IntegerField(default=0)
|
|
218
|
+
|
|
219
|
+
class Meta:
|
|
220
|
+
db_table = '"automations"."actions"'
|
|
221
|
+
app_label = 'constec_db'
|
|
222
|
+
indexes = [
|
|
223
|
+
models.Index(fields=['automation', 'order']),
|
|
224
|
+
models.Index(fields=['action_type', 'is_enabled']),
|
|
225
|
+
]
|
|
226
|
+
ordering = ['order', 'created_at']
|
|
227
|
+
|
|
228
|
+
def __str__(self):
|
|
229
|
+
return f"{self.action_type}: {self.name}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ExecutionLog(UUIDModel):
|
|
233
|
+
"""
|
|
234
|
+
Log de ejecución de una automatización.
|
|
235
|
+
|
|
236
|
+
Registra cada ejecución con su estado, duración, errores y detalles
|
|
237
|
+
de cada acción ejecutada.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
STATUS_CHOICES = [
|
|
241
|
+
('pending', 'Pending'),
|
|
242
|
+
('running', 'Running'),
|
|
243
|
+
('completed', 'Completed'),
|
|
244
|
+
('failed', 'Failed'),
|
|
245
|
+
('partial', 'Partial'), # Algunas acciones fallaron
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
TRIGGERED_BY_CHOICES = [
|
|
249
|
+
('schedule', 'Schedule'),
|
|
250
|
+
('webhook', 'Webhook'),
|
|
251
|
+
('manual', 'Manual'),
|
|
252
|
+
('system', 'System'),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# FKs
|
|
256
|
+
automation = models.ForeignKey(
|
|
257
|
+
Automation,
|
|
258
|
+
on_delete=models.CASCADE,
|
|
259
|
+
related_name='execution_logs'
|
|
260
|
+
)
|
|
261
|
+
trigger = models.ForeignKey(
|
|
262
|
+
Trigger,
|
|
263
|
+
on_delete=models.SET_NULL,
|
|
264
|
+
null=True,
|
|
265
|
+
blank=True,
|
|
266
|
+
related_name='execution_logs'
|
|
267
|
+
)
|
|
268
|
+
triggered_by_user = models.ForeignKey(
|
|
269
|
+
User,
|
|
270
|
+
on_delete=models.SET_NULL,
|
|
271
|
+
null=True,
|
|
272
|
+
blank=True,
|
|
273
|
+
related_name='triggered_executions'
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Estado
|
|
277
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
278
|
+
triggered_by = models.CharField(max_length=20, choices=TRIGGERED_BY_CHOICES)
|
|
279
|
+
|
|
280
|
+
# Timing
|
|
281
|
+
started_at = models.DateTimeField(auto_now_add=True)
|
|
282
|
+
completed_at = models.DateTimeField(null=True, blank=True)
|
|
283
|
+
duration_seconds = models.IntegerField(null=True, blank=True)
|
|
284
|
+
|
|
285
|
+
# Contexto de ejecución
|
|
286
|
+
execution_context = models.JSONField(
|
|
287
|
+
default=dict,
|
|
288
|
+
help_text="Variables y contexto disponible durante la ejecución"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Estadísticas de acciones
|
|
292
|
+
actions_total = models.IntegerField(default=0)
|
|
293
|
+
actions_succeeded = models.IntegerField(default=0)
|
|
294
|
+
actions_failed = models.IntegerField(default=0)
|
|
295
|
+
actions_skipped = models.IntegerField(default=0)
|
|
296
|
+
|
|
297
|
+
# Log detallado de cada acción
|
|
298
|
+
action_logs = models.JSONField(
|
|
299
|
+
default=list,
|
|
300
|
+
help_text="Array de logs detallados de cada acción ejecutada"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Errores
|
|
304
|
+
error_message = models.TextField(blank=True)
|
|
305
|
+
error_traceback = models.TextField(blank=True)
|
|
306
|
+
|
|
307
|
+
class Meta:
|
|
308
|
+
db_table = '"automations"."execution_logs"'
|
|
309
|
+
app_label = 'constec_db'
|
|
310
|
+
indexes = [
|
|
311
|
+
models.Index(fields=['automation', 'started_at']),
|
|
312
|
+
models.Index(fields=['status', 'started_at']),
|
|
313
|
+
models.Index(fields=['triggered_by', 'started_at']),
|
|
314
|
+
]
|
|
315
|
+
ordering = ['-started_at']
|
|
316
|
+
|
|
317
|
+
def __str__(self):
|
|
318
|
+
return f"{self.automation.name} - {self.status} ({self.started_at})"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class NotificationTemplate(UUIDModel):
|
|
322
|
+
"""
|
|
323
|
+
Template para notificaciones reutilizables.
|
|
324
|
+
|
|
325
|
+
Soporta variables en formato {{variable_name}} que se reemplazan
|
|
326
|
+
en tiempo de ejecución.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
CHANNEL_CHOICES = [
|
|
330
|
+
('email', 'Email'),
|
|
331
|
+
('whatsapp', 'WhatsApp'),
|
|
332
|
+
('sms', 'SMS'),
|
|
333
|
+
('push', 'Push Notification'),
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
# FKs
|
|
337
|
+
company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
|
|
338
|
+
created_by = models.ForeignKey(
|
|
339
|
+
User,
|
|
340
|
+
on_delete=models.CASCADE,
|
|
341
|
+
related_name='created_templates'
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Info básica
|
|
345
|
+
name = models.CharField(max_length=255)
|
|
346
|
+
description = models.TextField(blank=True)
|
|
347
|
+
channel = models.CharField(max_length=20, choices=CHANNEL_CHOICES)
|
|
348
|
+
|
|
349
|
+
# Contenido
|
|
350
|
+
subject = models.CharField(
|
|
351
|
+
max_length=255,
|
|
352
|
+
blank=True,
|
|
353
|
+
help_text="Asunto (solo para email)"
|
|
354
|
+
)
|
|
355
|
+
body = models.TextField(help_text="Cuerpo del mensaje con variables {{variable}}")
|
|
356
|
+
|
|
357
|
+
# Variables esperadas
|
|
358
|
+
variables = models.JSONField(
|
|
359
|
+
default=list,
|
|
360
|
+
help_text="Array de nombres de variables esperadas ['customer_name', 'amount']"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Estado
|
|
364
|
+
is_active = models.BooleanField(default=True)
|
|
365
|
+
|
|
366
|
+
class Meta:
|
|
367
|
+
db_table = '"automations"."notification_templates"'
|
|
368
|
+
app_label = 'constec_db'
|
|
369
|
+
indexes = [
|
|
370
|
+
models.Index(fields=['company', 'channel', 'is_active']),
|
|
371
|
+
]
|
|
372
|
+
ordering = ['name']
|
|
373
|
+
|
|
374
|
+
def __str__(self):
|
|
375
|
+
return f"{self.channel}: {self.name}"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class Event(UUIDModel):
|
|
379
|
+
"""
|
|
380
|
+
Evento programado con soporte para múltiples tipos y recurrencia flexible.
|
|
381
|
+
|
|
382
|
+
Tipos soportados:
|
|
383
|
+
- notification: Notificaciones programadas
|
|
384
|
+
- automation: Automatizaciones programadas (acciones HTTP, workflows, etc.)
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
EVENT_TYPE_CHOICES = [
|
|
388
|
+
('notification', 'Notification'),
|
|
389
|
+
('automation', 'Automation'),
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
STATUS_CHOICES = [
|
|
393
|
+
('active', 'Active'), # Activo y programado
|
|
394
|
+
('paused', 'Paused'), # Pausado temporalmente
|
|
395
|
+
('completed', 'Completed'), # Completado (para puntuales)
|
|
396
|
+
('failed', 'Failed'), # Falló la última ejecución
|
|
397
|
+
('cancelled', 'Cancelled'), # Cancelado permanentemente
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
RECURRENCE_TYPE_CHOICES = [
|
|
401
|
+
('punctual', 'Punctual'), # Una o más veces en fechas específicas
|
|
402
|
+
('periodic', 'Periodic'), # Recurrente
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
# FKs
|
|
406
|
+
company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
|
|
407
|
+
|
|
408
|
+
# Tipo de evento
|
|
409
|
+
event_type = models.CharField(max_length=50, choices=EVENT_TYPE_CHOICES)
|
|
410
|
+
|
|
411
|
+
# Info básica
|
|
412
|
+
title = models.CharField(max_length=255)
|
|
413
|
+
description = models.TextField(blank=True)
|
|
414
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
|
415
|
+
|
|
416
|
+
# Recurrencia
|
|
417
|
+
recurrence_type = models.CharField(
|
|
418
|
+
max_length=20,
|
|
419
|
+
choices=RECURRENCE_TYPE_CHOICES,
|
|
420
|
+
help_text="Tipo de recurrencia: puntual (fechas específicas) o periódico"
|
|
421
|
+
)
|
|
422
|
+
recurrence_config = models.JSONField(
|
|
423
|
+
help_text="Configuración de recurrencia en formato JSON"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Configuración específica del tipo de evento
|
|
427
|
+
config = models.JSONField(
|
|
428
|
+
default=dict,
|
|
429
|
+
help_text="Configuración específica según event_type (mensaje, URL, etc.)"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Control de ejecución
|
|
433
|
+
next_execution_at = models.DateTimeField(
|
|
434
|
+
null=True,
|
|
435
|
+
blank=True,
|
|
436
|
+
db_index=True,
|
|
437
|
+
help_text="Próxima fecha/hora de ejecución calculada"
|
|
438
|
+
)
|
|
439
|
+
last_executed_at = models.DateTimeField(
|
|
440
|
+
null=True,
|
|
441
|
+
blank=True,
|
|
442
|
+
help_text="Última vez que se ejecutó"
|
|
443
|
+
)
|
|
444
|
+
execution_count = models.IntegerField(
|
|
445
|
+
default=0,
|
|
446
|
+
help_text="Cantidad de veces que se ha ejecutado"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Auditoría
|
|
450
|
+
created_by = models.ForeignKey(
|
|
451
|
+
User,
|
|
452
|
+
on_delete=models.CASCADE,
|
|
453
|
+
related_name='created_events'
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
class Meta:
|
|
457
|
+
db_table = '"automations"."events"'
|
|
458
|
+
app_label = 'constec_db'
|
|
459
|
+
indexes = [
|
|
460
|
+
models.Index(fields=['company', 'event_type']),
|
|
461
|
+
models.Index(fields=['status', 'next_execution_at']),
|
|
462
|
+
models.Index(fields=['recurrence_type']),
|
|
463
|
+
]
|
|
464
|
+
ordering = ['next_execution_at']
|
|
465
|
+
|
|
466
|
+
def __str__(self):
|
|
467
|
+
return f"{self.event_type}: {self.title}"
|
|
@@ -13,8 +13,11 @@ constec/db/migrations/0001_initial.py
|
|
|
13
13
|
constec/db/migrations/0002_module_level.py
|
|
14
14
|
constec/db/migrations/0003_remove_module_level.py
|
|
15
15
|
constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py
|
|
16
|
+
constec/db/migrations/0005_event.py
|
|
17
|
+
constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py
|
|
16
18
|
constec/db/migrations/__init__.py
|
|
17
19
|
constec/db/models/__init__.py
|
|
20
|
+
constec/db/models/automation.py
|
|
18
21
|
constec/db/models/base.py
|
|
19
22
|
constec/db/models/company.py
|
|
20
23
|
constec/db/models/contact.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "constec"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Base library for the Constec ecosystem - shared utilities, models, and namespace foundation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|