simo 2.8.3__py3-none-any.whl → 2.8.4__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.

Potentially problematic release.


This version of simo might be problematic. Click here for more details.

@@ -6,7 +6,6 @@ import json
6
6
  import time
7
7
  import multiprocessing
8
8
  import threading
9
- import traceback
10
9
  from django.conf import settings
11
10
  from django.utils import timezone
12
11
  from django.db import connection as db_connection
@@ -43,10 +42,7 @@ class ScriptRunHandler(multiprocessing.Process):
43
42
  def run(self):
44
43
  db_connection.connect()
45
44
  self.component = Component.objects.get(id=self.component_id)
46
- try:
47
- tz = pytz.timezone(self.component.zone.instance.timezone)
48
- except:
49
- tz = pytz.timezone('UTC')
45
+ tz = pytz.timezone(self.component.zone.instance.timezone)
50
46
  timezone.activate(tz)
51
47
  introduce_instance(self.component.zone.instance)
52
48
  self.logger = get_component_logger(self.component)
@@ -235,8 +231,10 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
235
231
  def watch_scripts(self):
236
232
  drop_current_instance()
237
233
  # observe running scripts and drop the ones that are no longer alive
238
- dead_scripts = False
239
- for id, process in list(self.running_scripts.items()):
234
+ for id, data in list(self.running_scripts.items()):
235
+ if time.time() - data['start_time'] < 5:
236
+ continue
237
+ process = data['proc']
240
238
 
241
239
  comp = Component.objects.filter(id=id).first()
242
240
  if comp and comp.value == 'finished':
@@ -252,12 +250,18 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
252
250
  self.running_scripts.pop(id)
253
251
  continue
254
252
  else:
253
+ # it as been observed that is_alive might sometimes report false
254
+ # however the process is actually still running
255
+ process.kill()
255
256
  self.last_death = time.time()
256
257
  self.running_scripts.pop(id, None) # no longer running for sure!
257
258
  if not comp or comp.value != 'running':
258
259
  continue
259
260
 
260
261
  if id not in self.terminating_scripts: # was not intentionaly terminated
262
+ if comp:
263
+ tz = pytz.timezone(comp.zone.instance.timezone)
264
+ timezone.activate(tz)
261
265
  logger = get_component_logger(comp)
262
266
  logger.log(logging.INFO, "-------DEAD!-------")
263
267
  comp.value = 'error'
@@ -348,30 +352,35 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
348
352
 
349
353
  def start_script(self, component):
350
354
  print("START SCRIPT %s" % str(component))
355
+
351
356
  if component.id in self.running_scripts:
352
- if component.value in ('finished', 'error', 'stopped'):
353
- try:
354
- self.running_scripts[component.id].kill()
355
- except:
356
- pass
357
- self.running_scripts.pop(component.id, None)
358
- elif component.id not in self.terminating_scripts \
359
- and self.running_scripts[component.id].is_alive():
357
+ # it appears that the script is running and is perfectly healthy
358
+ # so we make sure it has correct value, do nothing else and return!
359
+ if component.id not in self.terminating_scripts \
360
+ and self.running_scripts[component.id]['proc'].is_alive():
360
361
  if component.value != 'running':
361
362
  component.value = 'running'
362
363
  component.save()
363
364
  return
364
- else:
365
- try:
366
- self.running_scripts[component.id].kill()
367
- except:
368
- pass
369
365
 
370
- self.running_scripts[component.id] = ScriptRunHandler(
371
- component.id, multiprocessing.Event(),
372
- daemon=True
366
+ # script is in terminating state or is no longer alive
367
+ # since starting of a new script was requested, we kill it viciously
368
+ # and continue on!
369
+ try:
370
+ self.running_scripts[component.id]['proc'].kill()
371
+ except:
372
+ pass
373
+ self.running_scripts.pop(component.id, None)
374
+
375
+
376
+ process = ScriptRunHandler(
377
+ component.id, multiprocessing.Event(), daemon=True
373
378
  )
374
- self.running_scripts[component.id].start()
379
+ process.start()
380
+ self.running_scripts[component.id] = {
381
+ 'proc': process, 'start_time': time.time()
382
+ }
383
+
375
384
 
376
385
  def stop_script(self, component, stop_status='stopped'):
377
386
  self.terminating_scripts.add(component.id)
@@ -389,17 +398,17 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
389
398
  elif stop_status == 'stopped':
390
399
  logger.log(logging.INFO, "-------STOP-------")
391
400
 
392
- if self.running_scripts[component.id].exit_in_use.is_set()\
393
- and not self.running_scripts[component.id].exin_in_use_fail.is_set():
394
- self.running_scripts[component.id].exit_event.set()
401
+ if self.running_scripts[component.id]['proc'].exit_in_use.is_set()\
402
+ and not self.running_scripts[component.id]['proc'].exin_in_use_fail.is_set():
403
+ self.running_scripts[component.id]['proc'].exit_event.set()
395
404
  else:
396
- self.running_scripts[component.id].terminate()
405
+ self.running_scripts[component.id]['proc'].terminate()
397
406
 
398
407
  def kill():
399
408
  start = time.time()
400
409
  terminated = False
401
410
  while start > time.time() - 2:
402
- if not self.running_scripts[component.id].is_alive():
411
+ if not self.running_scripts[component.id]['proc'].is_alive():
403
412
  terminated = True
404
413
  break
405
414
  time.sleep(0.1)
@@ -408,12 +417,12 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
408
417
  logger.log(logging.INFO, "-------GATEWAY KILL-------")
409
418
  else:
410
419
  logger.log(logging.INFO, "-------KILL!-------")
411
- self.running_scripts[component.id].kill()
420
+ self.running_scripts[component.id]['proc'].kill()
412
421
 
413
422
  component.set(stop_status)
414
423
  self.terminating_scripts.remove(component.id)
415
424
  # making sure it's fully killed along with it's child processes
416
- self.running_scripts[component.id].kill()
425
+ self.running_scripts[component.id]['proc'].kill()
417
426
  self.running_scripts.pop(component.id, None)
418
427
  logger.handlers = []
419
428
 
simo/core/controllers.py CHANGED
@@ -289,6 +289,8 @@ class ControllerBase(ABC):
289
289
  actor = self._get_actor(value)
290
290
  if not actor:
291
291
  actor = get_current_user()
292
+ if actor:
293
+ introduce_user(actor)
292
294
 
293
295
  self.component.refresh_from_db()
294
296
  if value != self.component.value:
simo/generic/forms.py CHANGED
@@ -157,6 +157,11 @@ class ThermostatConfigForm(BaseComponentForm):
157
157
 
158
158
  class AlarmBreachEventForm(forms.Form):
159
159
  uid = HiddenField(required=False)
160
+ threshold = forms.IntegerField(
161
+ label="Number of sensors breached",
162
+ min_value=1, max_value=5, initial=1,
163
+ help_text="React when at least given amount of sensors were breached."
164
+ )
160
165
  component = Select2ModelChoiceField(
161
166
  queryset=Component.objects.all(),
162
167
  url='autocomplete-component',
@@ -173,6 +178,7 @@ class AlarmBreachEventForm(forms.Form):
173
178
  help_text="Event will not fire if alarm group is disarmed "
174
179
  "within given timeframe of seconds after the breach."
175
180
  )
181
+
176
182
  prefix = 'breach_events'
177
183
 
178
184
  def clean(self):
simo/generic/gateways.py CHANGED
@@ -185,8 +185,6 @@ class GenericGatewayHandler(
185
185
  ('watch_thermostats', 60),
186
186
  ('watch_alarm_clocks', 30),
187
187
  ('watch_watering', 60),
188
- ('watch_alarm_events', 1),
189
- ('watch_timers', 1),
190
188
  ('watch_main_states', 60),
191
189
  ('watch_groups', 60)
192
190
  )
@@ -328,42 +326,6 @@ class GenericGatewayHandler(
328
326
  other_group.refresh_status()
329
327
 
330
328
 
331
- def watch_alarm_events(self):
332
- from .controllers import AlarmGroup
333
- drop_current_instance()
334
- for alarm in Component.objects.filter(
335
- controller_uid=AlarmGroup.uid, value='breached',
336
- meta__breach_start__gt=0
337
- ):
338
- for uid, event in alarm.controller.events_map.items():
339
- if uid in alarm.meta.get('events_triggered', []):
340
- continue
341
- if time.time() - alarm.meta['breach_start'] < event['delay']:
342
- continue
343
- try:
344
- getattr(event['component'], event['breach_action'])()
345
- except Exception:
346
- print(traceback.format_exc(), file=sys.stderr)
347
- if not alarm.meta.get('events_triggered'):
348
- alarm.meta['events_triggered'] = [uid]
349
- else:
350
- alarm.meta['events_triggered'].append(uid)
351
- alarm.save(update_fields=['meta'])
352
-
353
- def watch_timers(self):
354
- drop_current_instance()
355
- for component in Component.objects.filter(
356
- meta__timer_to__gt=0
357
- ).filter(meta__timer_to__lt=time.time()):
358
- component.meta['timer_to'] = 0
359
- component.meta['timer_start'] = 0
360
- component.save()
361
- try:
362
- component.controller._on_timer_end()
363
- except Exception as e:
364
- print(traceback.format_exc(), file=sys.stderr)
365
-
366
-
367
329
  def set_get_day_evening_night_morning(self, state):
368
330
  if state.value not in ('day', 'night', 'evening', 'morning'):
369
331
  return
simo/generic/models.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import time
2
2
  import sys
3
3
  import traceback
4
- from threading import Timer
5
4
  from django.db.models.signals import pre_save, post_save, post_delete
6
5
  from django.dispatch import receiver
7
- from simo.core.models import Instance, Component
6
+ from simo.core.models import Component
8
7
  from simo.users.models import InstanceUser
9
-
8
+ from .tasks import (
9
+ notify_users_on_alarm_group_breach,
10
+ fire_breach_events
11
+ )
10
12
 
11
13
 
12
14
  @receiver(post_save, sender=Component)
@@ -21,7 +23,7 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
21
23
 
22
24
  from .controllers import AlarmGroup
23
25
 
24
- for alarm_group in Component.objects.filter(
26
+ for ag in Component.objects.filter(
25
27
  controller_uid=AlarmGroup.uid,
26
28
  config__components__contains=instance.id,
27
29
  ).exclude(value='disarmed'):
@@ -30,48 +32,44 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
30
32
  }
31
33
  stats[instance.arm_status] += 1
32
34
  for slave in Component.objects.filter(
33
- pk__in=alarm_group.config['components'],
35
+ pk__in=ag.config['components'],
34
36
  ).exclude(pk=instance.pk):
35
37
  stats[slave.arm_status] += 1
36
- alarm_group.config['stats'] = stats
37
- alarm_group.save(update_fields=['config'])
38
38
 
39
- if stats['disarmed'] == len(alarm_group.config['components']):
39
+ print(f"STATS OF {ag} are: {stats}")
40
+ ag.config['stats'] = stats
41
+
42
+
43
+ if stats['disarmed'] == len(ag.config['components']):
40
44
  alarm_group_value = 'disarmed'
41
- elif stats['armed'] == len(alarm_group.config['components']):
45
+ elif stats['armed'] == len(ag.config['components']):
42
46
  alarm_group_value = 'armed'
43
47
  elif stats['breached']:
44
- if alarm_group.value != 'breached':
45
- def notify_users_security_breach(alarm_group_component_id):
46
- alarm_group_component = Component.objects.filter(
47
- id=alarm_group_component_id, value='breached'
48
- ).first()
49
- if not alarm_group_component:
50
- return
51
- breached_components = Component.objects.filter(
52
- pk__in=alarm_group_component.config['components'],
53
- arm_status='breached'
54
- )
55
- body = "Security Breach! " + '; '.join(
56
- [str(c) for c in breached_components]
57
- )
58
- from simo.notifications.utils import notify_users
59
- notify_users(
60
- 'alarm', str(alarm_group_component), body,
61
- component=alarm_group_component
62
- )
63
- if alarm_group.config.get('notify_on_breach') is not None:
64
- t = Timer(
65
- # give it one second to finish with other db processes.
66
- alarm_group.config['notify_on_breach'] + 1,
67
- notify_users_security_breach, [alarm_group.id]
68
- )
69
- t.start()
70
48
  alarm_group_value = 'breached'
71
49
  else:
72
50
  alarm_group_value = 'pending-arm'
73
51
 
74
- alarm_group.controller.set(alarm_group_value)
52
+ print(f"{ag} value: {alarm_group_value}")
53
+
54
+ if alarm_group_value == 'breached' and instance.arm_status == 'breached':
55
+ if ag.value != 'breached':
56
+ ag.meta['breach_times'] = [time.time()]
57
+ else:
58
+ ag.meta['breach_times'].append(time.time())
59
+
60
+ ag.save(update_fields=['meta', 'config'])
61
+ ag.controller.set(alarm_group_value)
62
+
63
+ if alarm_group_value == 'breached' and instance.arm_status == 'breached':
64
+ for event in ag.config['breach_events']:
65
+ if event['uid'] in ag.meta.get('events_triggered', []):
66
+ continue
67
+ threshold = event.get('threshold', 1)
68
+ if len(ag.meta['breach_times']) < threshold:
69
+ continue
70
+ fire_breach_events.apply_async(
71
+ args=[ag.id], countdown=event['delay']
72
+ )
75
73
 
76
74
 
77
75
  @receiver(pre_save, sender=Component)
@@ -85,8 +83,14 @@ def manage_alarm_groups(sender, instance, *args, **kwargs):
85
83
  return
86
84
 
87
85
  if instance.value == 'breached':
88
- instance.meta['breach_start'] = time.time()
89
86
  instance.meta['events_triggered'] = []
87
+
88
+ if instance.config.get('notify_on_breach') is not None:
89
+ notify_users_on_alarm_group_breach.apply_async(
90
+ args=[instance.id],
91
+ countdown=instance.config['notify_on_breach']
92
+ )
93
+
90
94
  elif instance.get_dirty_fields()['value'] == 'breached' \
91
95
  and instance.value == 'disarmed':
92
96
  instance.meta['breach_start'] = None
simo/generic/tasks.py ADDED
@@ -0,0 +1,80 @@
1
+ import sys, traceback, time
2
+ from celeryc import celery_app
3
+ from simo.core.middleware import drop_current_instance, drop_current_instance
4
+
5
+
6
+ @celery_app.task
7
+ def notify_users_on_alarm_group_breach(ag_id):
8
+ from simo.core.models import Component
9
+ drop_current_instance()
10
+ ag = Component.objects.filter(id=ag_id).first()
11
+ if not ag:
12
+ return
13
+ if ag.value != 'breached':
14
+ # no longer breached, somebody disarmed it,
15
+ # no need to send any notifications
16
+ return
17
+
18
+ breached_components = Component.objects.filter(
19
+ pk__in=ag.config['components'],
20
+ arm_status='breached'
21
+ )
22
+ body = "Security Breach! " + '; '.join(
23
+ [str(c) for c in breached_components]
24
+ )
25
+ from simo.notifications.utils import notify_users
26
+ notify_users(
27
+ 'alarm', str(ag), body, component=ag,
28
+ instance=ag.zone.instance
29
+ )
30
+
31
+
32
+ @celery_app.task
33
+ def fire_breach_events(ag_id):
34
+ from simo.core.models import Component
35
+ drop_current_instance()
36
+ ag = Component.objects.filter(id=ag_id).first()
37
+ if not ag:
38
+ return
39
+ if ag.value != 'breached':
40
+ # no longer breached, somebody disarmed it,
41
+ # no need to send any notifications
42
+ return
43
+ for uid, event in ag.controller.events_map.items():
44
+ if uid in ag.meta.get('events_triggered', []):
45
+ continue
46
+ threshold = event.get('threshold', 1)
47
+ if len(ag.meta['breach_times']) < threshold:
48
+ continue
49
+ if time.time() - ag.meta['breach_times'][threshold - 1] < event['delay']:
50
+ continue
51
+ try:
52
+ getattr(event['component'], event['breach_action'])()
53
+ except Exception:
54
+ print(traceback.format_exc(), file=sys.stderr)
55
+ if not ag.meta.get('events_triggered'):
56
+ ag.meta['events_triggered'] = [uid]
57
+ else:
58
+ ag.meta['events_triggered'].append(uid)
59
+ ag.save(update_fields=['meta'])
60
+
61
+
62
+ @celery_app.task
63
+ def watch_timers():
64
+ from simo.core.models import Component
65
+ drop_current_instance()
66
+ for component in Component.objects.filter(
67
+ meta__timer_to__gt=0
68
+ ).filter(meta__timer_to__lt=time.time()):
69
+ component.meta['timer_to'] = 0
70
+ component.meta['timer_start'] = 0
71
+ component.save()
72
+ try:
73
+ component.controller._on_timer_end()
74
+ except Exception as e:
75
+ print(traceback.format_exc(), file=sys.stderr)
76
+
77
+
78
+ @celery_app.on_after_finalize.connect
79
+ def setup_periodic_tasks(sender, **kwargs):
80
+ sender.add_periodic_task(1, watch_timers.s())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: simo
3
- Version: 2.8.3
3
+ Version: 2.8.4
4
4
  Summary: Smart Home on Steroids!
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -21,7 +21,7 @@ simo/automation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  simo/automation/app_widgets.py,sha256=gaqImMZjuMHm7nIb9a4D-Y3qipz_WhSPAHXcwGx4Uzs,199
22
22
  simo/automation/controllers.py,sha256=66aGjlfgigcePXiLmDkbVrxbm-Z26klMKIiOvaMH71k,11545
23
23
  simo/automation/forms.py,sha256=UWnkxw8pILPK0smRPTo4SLgsZl78zOySx7JIc30Bgtk,10228
24
- simo/automation/gateways.py,sha256=MAzv-i8BemhqfNY2NTYYlyCGPfctyNh4_dXyIZYJRSE,15848
24
+ simo/automation/gateways.py,sha256=8qgBDhkM_xYGmeEbnH5WXs5m3ihMp8Yzx7yj6XG8DNM,16382
25
25
  simo/automation/helpers.py,sha256=iP-fxxB8HsFQy3k2CjFubu86aMqvWgmh-p24DiyOrek,4330
26
26
  simo/automation/models.py,sha256=zt-jkzyq5ddqGT864OkJzCsvov2vZ0nO4ez3hAeZkXg,934
27
27
  simo/automation/serializers.py,sha256=PjyFrjdPK1mBsgbNhyqMi9SWzcymqTa742ipy0LhAN4,1996
@@ -34,7 +34,7 @@ simo/automation/__pycache__/controllers.cpython-312.pyc,sha256=QV63g3UlpMAA-yCaZ
34
34
  simo/automation/__pycache__/controllers.cpython-38.pyc,sha256=CL-0Tq9B4-E36fYfWT1XEBTq1dkq1W8003f6MrBnQU0,8391
35
35
  simo/automation/__pycache__/forms.cpython-312.pyc,sha256=63rU0rWZk-Rz5qoMZiXl743WPc9NVm5d8bSd8w52T4E,12347
36
36
  simo/automation/__pycache__/forms.cpython-38.pyc,sha256=cpA5hA2Iz3JsPC0Dq01ki1I7S9c5DKRcXveHApI1dJo,7772
37
- simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=j4bG9KqBcCB8JD59frTl9VtP8n04mUPF_bd0vInDZfs,22223
37
+ simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=mQWn7s8qqF-CIw9Zn80RbbaFJjTiCdaN_grMsfwa-nY,22405
38
38
  simo/automation/__pycache__/gateways.cpython-38.pyc,sha256=nHujqChMCqqxHbZezP3MisavjKDhczqzFGurO10h-lc,11113
39
39
  simo/automation/__pycache__/helpers.cpython-312.pyc,sha256=aDFtzBE72szi4gzVxK_NiAEC__wCmdztw0UKu2lVU58,5853
40
40
  simo/automation/__pycache__/helpers.cpython-38.pyc,sha256=fNjSyn4Mfq7-JQx-bdsnj-rSxgu20dVJ9-5ZEMT6yiM,3627
@@ -70,6 +70,7 @@ simo/backups/__pycache__/dynamic_settings.cpython-312.pyc,sha256=Sf8IorKxo6r3R0Q
70
70
  simo/backups/__pycache__/dynamic_settings.cpython-38.pyc,sha256=51gJFjn_XqQBRoHeubo6ppb9pNuFQKI5hAR0ms9flE8,731
71
71
  simo/backups/__pycache__/models.cpython-312.pyc,sha256=45nfTQzX8YuKI67KEe0nAdlZEQHAZjJu44NUr4PC5WI,1746
72
72
  simo/backups/__pycache__/models.cpython-38.pyc,sha256=zclX7HwOT4_izweyKNQ8LmgSHb3hNcYcfsSiwwfQoLY,1220
73
+ simo/backups/__pycache__/tasks.cpython-312.pyc,sha256=Vg95hEynVXVA89bkG0BN5Cw6KkuVCV4JCiKLsd7xI_8,18369
73
74
  simo/backups/__pycache__/tasks.cpython-38.pyc,sha256=bKz_Rxt_H0lC0d9_4Dxqv7cirQDSH9LVurZZC0LU94s,9179
74
75
  simo/backups/migrations/0001_initial.py,sha256=0LzCusTUyYf61ksiepdnqXIuYYNTNd_djh_Wa484HA0,770
75
76
  simo/backups/migrations/0002_backuplog_backup_level_backup_size.py,sha256=w9T9MQWuecy91OZE1fMExriwPuXA8HMPKsPwXhmC8_k,1023
@@ -97,7 +98,7 @@ simo/core/auto_urls.py,sha256=FqKhH0fF7cGO6P2YrjblwG4JA2UkVXj3lreJUOB2Jq4,1194
97
98
  simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
98
99
  simo/core/base_types.py,sha256=WypW8hTfzveuTQtruGjLYAGQZIuczxTlW-SdRk3iQug,666
99
100
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
100
- simo/core/controllers.py,sha256=Cy0-F4QEo-RbVkRpdfvl0sah9cUs4InP1T8shojaiKY,37355
101
+ simo/core/controllers.py,sha256=4u67VNwU4Zts64aVddqhoPt9ZRbpTmAHPCdu-pFfIX0,37407
101
102
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
102
103
  simo/core/events.py,sha256=1_KIk5pJqdLPRQlCQ9xSyALst2Cn0b2lAEAJ3QjwIjE,4801
103
104
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
@@ -141,7 +142,7 @@ simo/core/__pycache__/base_types.cpython-312.pyc,sha256=Lnq2NL9B5hfwJARJYC447Rdv
141
142
  simo/core/__pycache__/base_types.cpython-38.pyc,sha256=CX-qlF7CefRi_mCE954wYa9rUFR88mOl6g7fybDRu7g,803
142
143
  simo/core/__pycache__/context.cpython-312.pyc,sha256=8rsN2Er-Sx3rrVmO0Gk4cem3euGh0kTELXj667GGZ5E,2193
143
144
  simo/core/__pycache__/context.cpython-38.pyc,sha256=NlTHt2GvXxA21AhBkeyOLfRFUuXw7wmwqyNhhcDl2cw,1373
144
- simo/core/__pycache__/controllers.cpython-312.pyc,sha256=4QDs4cfrxPLcSnJLZFNn0XsGyvAy4GzNCVMr-qljNbM,52855
145
+ simo/core/__pycache__/controllers.cpython-312.pyc,sha256=pNjKIaSvzEWJlI6_yQdmakyFYNgrxv6dyexVvoScUj4,52897
145
146
  simo/core/__pycache__/controllers.cpython-38.pyc,sha256=LtrQQ8egOIOuQbAckeM-z8OfbzS4W8VQ3vBnryAm3iU,32086
146
147
  simo/core/__pycache__/dynamic_settings.cpython-312.pyc,sha256=WUZ6XF4kZb6zPf541PkKmiQaBIw-r5C6F3EUUZiTEnE,3331
147
148
  simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=wGpnscX1DxFpRl54MQURhjz2aD3NJohSzw9JCFnzh2Y,2384
@@ -10446,6 +10447,7 @@ simo/fleet/__pycache__/serializers.cpython-312.pyc,sha256=reKKBMohl7vi7iJ6wjRbOB
10446
10447
  simo/fleet/__pycache__/serializers.cpython-38.pyc,sha256=l_FzORWCM1hcSZV0AaGRO-p0CMTcEfqnLGgbn2IVvI0,3648
10447
10448
  simo/fleet/__pycache__/socket_consumers.cpython-312.pyc,sha256=1ZEKGEL8FDeALU-BLlap9rjHZKgc_FDtLr0INSWspGo,27221
10448
10449
  simo/fleet/__pycache__/socket_consumers.cpython-38.pyc,sha256=lEC1SkY_KgRY0QoBUMPjnbFwSa7qmCf-4eNQ45hAy68,14141
10450
+ simo/fleet/__pycache__/tasks.cpython-312.pyc,sha256=1XDir2GWxl7c17n0ZuCxOFKsLjaCqXgqVT4TcDvzhm8,1817
10449
10451
  simo/fleet/__pycache__/tasks.cpython-38.pyc,sha256=RoNxL2WUiW67s9O9DjaYVVjCBSZu2nje0Qn9FJkWVS0,1116
10450
10452
  simo/fleet/__pycache__/utils.cpython-312.pyc,sha256=mrICzm_po8QGUaRddn0TvBp5RDLBe1Jklg7xZr0YduM,4995
10451
10453
  simo/fleet/__pycache__/utils.cpython-38.pyc,sha256=obUd-X2Y-ybx4icqUWq_qwIxrP9yyarJjexWAfO4MTI,3344
@@ -10592,11 +10594,12 @@ simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10592
10594
  simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,856
10593
10595
  simo/generic/base_types.py,sha256=u3SlfpNYaCwkVBwomWgso4ODzL71ay9MhiAW-bxgnDU,341
10594
10596
  simo/generic/controllers.py,sha256=NZmrJ1BYzAwHJ1vN-OMvqnH13ihvjUhZqdxq-l__1Jk,46268
10595
- simo/generic/forms.py,sha256=yCEmOy6Sm5moCh-9UTzqLDiI5xmzfm5LwEhZ2r9KRbw,26054
10596
- simo/generic/gateways.py,sha256=d7sbt_GeeGGwp2hoD-FZNuyM6fNvEU51kiZyznZYcxg,17589
10597
- simo/generic/models.py,sha256=Adq7ipWK-renxJlNW-SZnAq2oGEOwKx8EdUWaKnfcVQ,7597
10597
+ simo/generic/forms.py,sha256=1Gt3FycP4Da8LbtOuOGTpLSf2dCDEE-Sdr_X3qlA1aw,26265
10598
+ simo/generic/gateways.py,sha256=kjqGMjlLXlGNreKLQtHoSnw4xan1IB8-KF8AqiZ82T0,16057
10599
+ simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
10598
10600
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10599
10601
  simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
10602
+ simo/generic/tasks.py,sha256=5jhi7Jv7lfaM3T8GArWKaDqQfuvdBsKqz5obN6NVUqk,2570
10600
10603
  simo/generic/__pycache__/__init__.cpython-312.pyc,sha256=-34GwswSg1zc1becA8lPwUpnd4ek2IoduCsIUr0KTNo,167
10601
10604
  simo/generic/__pycache__/__init__.cpython-38.pyc,sha256=mLu54WS9KIl-pHwVCBKpsDFIlOqml--JsOVzAUHg6cU,161
10602
10605
  simo/generic/__pycache__/app_widgets.cpython-312.pyc,sha256=ywoKk91YSEZxpyt9haG509_c0G9DMJVpae_y1iiZxJU,1937
@@ -10605,16 +10608,17 @@ simo/generic/__pycache__/base_types.cpython-312.pyc,sha256=h8Mwu49i-zmwTbL33JaLJ
10605
10608
  simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=aV5NdIuvXR-ItKpI__MwcyPZHD6Z882TFdgYkPCkr1I,493
10606
10609
  simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=TgHV-dNMJqYuFOgeJTxeh4Mihb4lHnfhid0CmdhZCR4,52737
10607
10610
  simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=jJjwKVaDYyazrRGNjUFoY74nr_jX_DEnsC9KjyxZCgc,30427
10608
- simo/generic/__pycache__/forms.cpython-312.pyc,sha256=duBW82wI9zfyzPnIk4hLs_vx5Rfk4vSCh9n7cSNbgcc,34681
10611
+ simo/generic/__pycache__/forms.cpython-312.pyc,sha256=nTyrgmEmOrgORJSOxUI_i2O7wVUXCQL9MYhaT76Jmew,34869
10609
10612
  simo/generic/__pycache__/forms.cpython-38.pyc,sha256=k8lz3taXdWAg5P9jcnw66mWH51pCc4SOsg32kVEtBCg,19416
10610
- simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=-ifxm5kYUvp6tRw3xr1EO7jCEXQ9_aQG3OdxAQvxHBg,21758
10613
+ simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=ixeotxPeqHkINziDnnkymlKR2wYMb5UR2sxiioMQxhY,19309
10611
10614
  simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=GIeMT51oZU2OCFD4eUDFdSRRYE0Qf14AcOr_gdUqG94,12705
10612
- simo/generic/__pycache__/models.cpython-312.pyc,sha256=xriUzjkaM2Y4mT3jo2OPK-XGBroBBSFJfLqK0jMA4MA,10200
10615
+ simo/generic/__pycache__/models.cpython-312.pyc,sha256=ggaeX6BQa-0-KG50HadpRCWeW84Fbog0muT2gBkqLNQ,10190
10613
10616
  simo/generic/__pycache__/models.cpython-38.pyc,sha256=MZpum7syAFxuulf47K7gtUlJJ7xRD-IBUBAwUM1ZRnw,5825
10614
10617
  simo/generic/__pycache__/routing.cpython-312.pyc,sha256=_wQPZeAgwlGtnafw9VcabgqjyJxzDFywHBIFbGhzYRE,452
10615
10618
  simo/generic/__pycache__/routing.cpython-38.pyc,sha256=xtxTUTBTdivzFyA5Wh7k-hUj1WDO_FiRq6HYXdbr9Ks,382
10616
10619
  simo/generic/__pycache__/socket_consumers.cpython-312.pyc,sha256=zERTr2bHXLKSXCoIov6MaFRrgfeS1A0IHCXbaPfVvK4,2814
10617
10620
  simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=FaVCf_uJI2uwj1Zz-jwsOXou65oV9gFCIB8e-YKquRk,1662
10621
+ simo/generic/__pycache__/tasks.cpython-312.pyc,sha256=zEyNpFVmEJoZdeYKNZ7cEmPtIyZMTwEqzZJZZwMYl-o,4494
10618
10622
  simo/generic/migrations/0001_initial.py,sha256=7FpPcfpRU5ya0b8s2KbxR5a3npf92YruvZltUybjzys,676
10619
10623
  simo/generic/migrations/0002_auto_20241126_0726.py,sha256=SX38JwP732QooOm5HM1Xo7Th_Mv_6YZloT3eozULOhs,922
10620
10624
  simo/generic/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10719,6 +10723,7 @@ simo/notifications/__pycache__/models.cpython-312.pyc,sha256=VKa2ZQxzBH22YAUlsmh
10719
10723
  simo/notifications/__pycache__/models.cpython-38.pyc,sha256=PoqLuOnlaAWQQ-20AtqhvAlLSkakPmdn7J7wGvHNW3g,2449
10720
10724
  simo/notifications/__pycache__/serializers.cpython-312.pyc,sha256=on-lu0Sj7vEl0_-2ul19Yk59gYHIFESXZKpA9RKNewQ,1226
10721
10725
  simo/notifications/__pycache__/serializers.cpython-38.pyc,sha256=7-eRGKYuQ4g1SpKOMpz17SIiu1HmaMoYv-cJbaO9QGA,1028
10726
+ simo/notifications/__pycache__/utils.cpython-312.pyc,sha256=o-CnxcAtX7tHo2CdGBKjLTd1OU3z57zUCaWlYzEuv88,2423
10722
10727
  simo/notifications/__pycache__/utils.cpython-38.pyc,sha256=4ZnI-pmWji84EXBkPrl4ir1kGjfanO4bH5--bLNRxCA,1648
10723
10728
  simo/notifications/migrations/0001_initial.py,sha256=Zh69AQ-EKlQKfqfnMDVRcxvo1MxRY-TFLCdnNcgqi6g,2003
10724
10729
  simo/notifications/migrations/0002_notification_instance.py,sha256=B3msbMeKvsuq-V7gvRADRjj5PFLayhi3pQvHZjqzO5g,563
@@ -10777,6 +10782,7 @@ simo/users/__pycache__/sso_urls.cpython-312.pyc,sha256=FQLOFu310j7pOhTNlg2wyUybq
10777
10782
  simo/users/__pycache__/sso_urls.cpython-38.pyc,sha256=uAwDozpOmrhUald-8tOHANILXkH7-TI8fNYXOtPkSY8,402
10778
10783
  simo/users/__pycache__/sso_views.cpython-312.pyc,sha256=FZGDRdt7pb8qROe8ZAEGSTfeZtLCGNBvQiXpDtrc4dE,6011
10779
10784
  simo/users/__pycache__/sso_views.cpython-38.pyc,sha256=PLRF6FYCxRhnmgnN_gUS-pdQlH7lofLU1Xhgw3vDO_Y,4019
10785
+ simo/users/__pycache__/tasks.cpython-312.pyc,sha256=cu-VV1HWXToHR875DFC2E9_x1cn1mD5mXYqltk0QOuw,1955
10780
10786
  simo/users/__pycache__/tasks.cpython-38.pyc,sha256=XLMKt3suT7BlcXrJZoH9ZIhhtBuqyiW4lsOB9IbBkko,1225
10781
10787
  simo/users/__pycache__/utils.cpython-312.pyc,sha256=dJtfFncpI2QtTSZoUUNCfUsDoofOOfVtMRXNm5ZXC20,3515
10782
10788
  simo/users/__pycache__/utils.cpython-38.pyc,sha256=cgxUwEaBgxT360mw4E03J7u4vi3dhw6K3n7-8WNvyFU,1888
@@ -10924,9 +10930,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10924
10930
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10925
10931
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10926
10932
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10927
- simo-2.8.3.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10928
- simo-2.8.3.dist-info/METADATA,sha256=e73yT_ZPrUBSQ9WeaC3C_Ha7RqPRz8oRME8ayRXL1X4,2008
10929
- simo-2.8.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10930
- simo-2.8.3.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10931
- simo-2.8.3.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10932
- simo-2.8.3.dist-info/RECORD,,
10933
+ simo-2.8.4.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10934
+ simo-2.8.4.dist-info/METADATA,sha256=R_KaZi61ln63mwg8ywrTAVIFTD5b_EI4tj1kGJtV6wI,2008
10935
+ simo-2.8.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10936
+ simo-2.8.4.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10937
+ simo-2.8.4.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10938
+ simo-2.8.4.dist-info/RECORD,,
File without changes