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

@@ -311,11 +311,19 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
311
311
 
312
312
  from .controllers import Script
313
313
 
314
- mqtt_client = mqtt.Client()
315
- mqtt_client.username_pw_set('root', settings.SECRET_KEY)
316
- mqtt_client.on_connect = self.on_mqtt_connect
317
- mqtt_client.on_message = self.on_mqtt_message
318
- mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
314
+ self.mqtt_client = mqtt.Client()
315
+ self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
316
+ self.mqtt_client.on_connect = self.on_mqtt_connect
317
+ self.mqtt_client.on_message = self.on_mqtt_message
318
+ try:
319
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
320
+ except Exception:
321
+ pass
322
+ try:
323
+ # Avoid raising if broker restarts or is down at boot
324
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
325
+ except Exception:
326
+ pass
319
327
 
320
328
  # We presume that this is the only running gateway, therefore
321
329
  # if there are any running scripts, that is not true.
@@ -337,9 +345,11 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
337
345
  self.start_script(script)
338
346
 
339
347
  print("GATEWAY STARTED!")
348
+ self.mqtt_client.loop_start()
340
349
  while not exit.is_set():
341
- mqtt_client.loop()
342
- mqtt_client.disconnect()
350
+ time.sleep(1)
351
+ self.mqtt_client.loop_stop()
352
+ self.mqtt_client.disconnect()
343
353
 
344
354
  script_ids = [id for id in self.running_scripts.keys()]
345
355
  for id in script_ids:
simo/core/controllers.py CHANGED
@@ -647,7 +647,10 @@ class Button(ControllerBase):
647
647
  default_value = 'up'
648
648
 
649
649
  def _validate_val(self, value, occasion=None):
650
- if value not in ('down', 'up', 'hold', 'click', 'double-click'):
650
+ if value not in (
651
+ 'down', 'up', 'hold',
652
+ 'click', 'double-click', 'triple-click', 'quadruple-click', 'quintuple-click'
653
+ ):
651
654
  raise ValidationError("Bad button value!")
652
655
  return value
653
656
 
simo/core/events.py CHANGED
@@ -181,9 +181,21 @@ class OnChangeMixin:
181
181
  self._mqtt_client.username_pw_set('root', settings.SECRET_KEY)
182
182
  self._mqtt_client.on_connect = self.on_mqtt_connect
183
183
  self._mqtt_client.on_message = self.on_mqtt_message
184
- self._mqtt_client.connect(
185
- host=settings.MQTT_HOST, port=settings.MQTT_PORT
186
- )
184
+ # Be gentle when broker is down or connection flaps
185
+ try:
186
+ # paho 1.x supports reconnect backoff configuration
187
+ self._mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
188
+ except Exception:
189
+ pass
190
+ try:
191
+ self._mqtt_client.connect_async(
192
+ host=settings.MQTT_HOST, port=settings.MQTT_PORT
193
+ )
194
+ except Exception:
195
+ # connect_async should not normally raise; in case of programming
196
+ # errors keep the API surface consistent by cleaning up
197
+ self._mqtt_client = None
198
+ raise
187
199
  self._mqtt_client.loop_start()
188
200
  self._on_change_function = function
189
201
  self._obj_ct_id = ContentType.objects.get_for_model(self).pk
simo/core/forms.py CHANGED
@@ -71,16 +71,45 @@ class CategoryAdminForm(forms.ModelForm):
71
71
  class ConfigFieldsMixin:
72
72
 
73
73
  def __init__(self, *args, **kwargs):
74
+ """Augment forms with dynamic controller fields and
75
+ persist non-model fields under component.config.
76
+
77
+ Dynamic fields are appended by controllers via
78
+ `controller._get_dynamic_config_fields()` and are excluded from
79
+ automatic component.config persistence. Controllers may handle
80
+ them in `_apply_dynamic_config()` during save.
81
+ """
74
82
  super().__init__(*args, **kwargs)
83
+
84
+ # Inject dynamic fields from controller (if any)
85
+ self._dynamic_fields = []
86
+ controller = getattr(self, 'controller', None)
87
+ if controller and hasattr(controller, '_get_dynamic_config_fields'):
88
+ try:
89
+ dyn_fields = controller._get_dynamic_config_fields() or {}
90
+ if isinstance(dyn_fields, dict):
91
+ for fname, field in dyn_fields.items():
92
+ if fname in self.fields:
93
+ continue
94
+ self.fields[fname] = field
95
+ self._dynamic_fields.append(fname)
96
+ except Exception:
97
+ # Never break the form if controller fails here
98
+ pass
99
+
100
+ # Build config-backed field list (exclude model fields and dynamic fields)
75
101
  self.model_fields = [
76
102
  f.name for f in Component._meta.fields
77
103
  ] + ['slaves', ]
78
104
  self.config_fields = []
79
- for field_name, field in self.fields.items():
105
+ for field_name in list(self.fields.keys()):
80
106
  if field_name in self.model_fields:
81
107
  continue
108
+ if field_name in self._dynamic_fields:
109
+ continue
82
110
  self.config_fields.append(field_name)
83
111
 
112
+ # Initialize config-backed fields from instance.config
84
113
  for field_name in self.config_fields:
85
114
  if field_name not in self.instance.config:
86
115
  continue
@@ -106,38 +135,46 @@ class ConfigFieldsMixin:
106
135
 
107
136
 
108
137
  def save(self, commit=True):
138
+ # Write config-backed fields under component.config
109
139
  for field_name in self.config_fields:
110
140
  # support for partial forms
111
141
  if field_name not in self.cleaned_data:
112
142
  continue
113
143
  if isinstance(self.cleaned_data[field_name], models.Model):
114
- self.instance.config[field_name] = \
115
- self.cleaned_data[field_name].pk
144
+ self.instance.config[field_name] = self.cleaned_data[field_name].pk
116
145
  elif isinstance(self.cleaned_data[field_name], models.QuerySet):
117
- self.instance.config[field_name] = [
118
- obj.pk for obj in self.cleaned_data[field_name]
119
- ]
146
+ self.instance.config[field_name] = [obj.pk for obj in self.cleaned_data[field_name]]
120
147
  else:
121
148
  try:
122
- self.instance.config[field_name] = \
123
- json.loads(json.dumps(self.cleaned_data[field_name]))
124
- except:
149
+ self.instance.config[field_name] = json.loads(json.dumps(self.cleaned_data[field_name]))
150
+ except Exception:
125
151
  continue
126
152
 
153
+ # Save component
127
154
  if commit:
128
155
  from simo.users.utils import get_current_user
129
156
  actor = get_current_user()
130
- if self.instance.pk:
131
- verb = 'modified'
132
- else:
133
- verb = 'created'
134
- action.send(
135
- actor, target=self.instance, verb=verb,
136
- instance_id=self.instance.zone.instance.id,
137
- action_type='management_event'
138
- )
157
+ if actor:
158
+ if self.instance.pk:
159
+ verb = 'modified'
160
+ else:
161
+ verb = 'created'
162
+ action.send(
163
+ actor, target=self.instance, verb=verb,
164
+ instance_id=self.instance.zone.instance.id,
165
+ action_type='management_event'
166
+ )
167
+ result = super().save(commit)
139
168
 
140
- return super().save(commit)
169
+ # Apply dynamic settings via controller hook
170
+ controller = getattr(self, 'controller', None)
171
+ if controller and hasattr(controller, '_apply_dynamic_config'):
172
+ try:
173
+ controller._apply_dynamic_config(self.cleaned_data)
174
+ except Exception:
175
+ pass
176
+
177
+ return result
141
178
 
142
179
 
143
180
  class BaseGatewayForm(ConfigFieldsMixin, forms.ModelForm):
@@ -329,10 +366,24 @@ class ComponentAdminForm(forms.ModelForm):
329
366
  base_fields.append('category')
330
367
  base_fields.append('show_in_app')
331
368
 
369
+ # Include statically declared fields first
332
370
  for field_name in cls.declared_fields:
333
371
  if field_name not in main_fields:
334
372
  base_fields.append(field_name)
335
373
 
374
+ # If editing an existing object, include any dynamic fields
375
+ # that the form instance adds at runtime.
376
+ if obj is not None:
377
+ try:
378
+ tmp_form = cls(request=request, instance=obj, controller_uid=obj.controller_uid)
379
+ for fname in tmp_form.fields.keys():
380
+ if fname in base_fields or fname in main_fields:
381
+ continue
382
+ base_fields.append(fname)
383
+ except Exception:
384
+ # Ignore dynamic field detection issues
385
+ pass
386
+
336
387
  base_fields.append('control')
337
388
  base_fields.append('notes')
338
389
 
@@ -389,6 +440,29 @@ class ComponentAdminForm(forms.ModelForm):
389
440
 
390
441
 
391
442
  class BaseComponentForm(ConfigFieldsMixin, ComponentAdminForm):
443
+ """Base form for all components.
444
+
445
+ Dynamic controller-backed fields
446
+ --------------------------------
447
+ Controllers can expose device-scoped, dynamic options by implementing
448
+ the following optional hooks on the controller instance:
449
+
450
+ - _get_dynamic_config_fields(self) -> dict[str, django.forms.Field]
451
+ Return a mapping of additional form fields to append at runtime.
452
+ These fields are not persisted automatically to component.config.
453
+ Set each field's `initial` to reflect the current device value.
454
+
455
+ - _apply_dynamic_config(self, form_data: dict) -> None
456
+ Called after the form is saved. Receives the full cleaned form
457
+ data (including dynamic fields). Use this to apply device options
458
+ (e.g. send commands to a gateway or write to component.config if
459
+ your integration needs to persist something custom).
460
+
461
+ Notes
462
+ - Dynamic fields are excluded from `basic_fields`, so only higher-level
463
+ users (instance superusers/masters) will see and edit them.
464
+ - All existing static fields and behavior remain unchanged.
465
+ """
392
466
  pass
393
467
 
394
468
 
simo/core/gateways.py CHANGED
@@ -63,6 +63,10 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
63
63
  self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
64
64
  self.mqtt_client.on_connect = self._on_mqtt_connect
65
65
  self.mqtt_client.on_message = self._on_mqtt_message
66
+ try:
67
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
68
+ except Exception:
69
+ pass
66
70
 
67
71
 
68
72
  def run(self, exit):
@@ -74,7 +78,12 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
74
78
  target=self._run_periodic_task, args=(self.exit, task, period), daemon=True
75
79
  ).start()
76
80
 
77
- self.mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
81
+ # Use async connect so we don't crash if broker is temporarily down
82
+ try:
83
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
84
+ except Exception:
85
+ # connect_async shouldn't raise for normal scenarios; ignore just in case
86
+ pass
78
87
  self.mqtt_client.loop_start()
79
88
 
80
89
  while not self.exit.is_set():
@@ -137,4 +146,3 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
137
146
  self.perform_value_send(component, val)
138
147
  except Exception as e:
139
148
  self.logger.error(e, exc_info=True)
140
-
@@ -117,18 +117,28 @@ class GatewaysManager:
117
117
  self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
118
118
  self.mqtt_client.on_connect = self.on_mqtt_connect
119
119
  self.mqtt_client.on_message = self.on_mqtt_message
120
- self.mqtt_client.connect(
121
- host=settings.MQTT_HOST, port=settings.MQTT_PORT
122
- )
120
+ try:
121
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
122
+ except Exception:
123
+ pass
124
+ try:
125
+ self.mqtt_client.connect_async(
126
+ host=settings.MQTT_HOST, port=settings.MQTT_PORT
127
+ )
128
+ except Exception:
129
+ pass
123
130
 
131
+ self.mqtt_client.loop_start()
124
132
  while not self.exit_event.is_set():
125
- self.mqtt_client.loop()
133
+ time.sleep(1)
126
134
 
127
135
  ids_to_stop = [id for id in self.running_gateways.keys()]
128
136
  for id in ids_to_stop:
129
137
  self.stop_gateway(Gateway(id=id))
130
138
  while self.running_gateways.keys():
131
139
  time.sleep(0.3)
140
+ self.mqtt_client.loop_stop()
141
+ self.mqtt_client.disconnect()
132
142
  close_old_connections()
133
143
  print("-------------Gateways Manager STOPPED.------------------")
134
144
  return sys.exit()
simo/generic/gateways.py CHANGED
@@ -297,11 +297,19 @@ class GenericGatewayHandler(
297
297
 
298
298
  from simo.generic.controllers import IPCamera
299
299
 
300
- mqtt_client = mqtt.Client()
301
- mqtt_client.username_pw_set('root', settings.SECRET_KEY)
302
- mqtt_client.on_connect = self.on_mqtt_connect
303
- mqtt_client.on_message = self.on_mqtt_message
304
- mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
300
+ # Use non-blocking MQTT loop to avoid busy-spin when broker is down
301
+ self.mqtt_client = mqtt.Client()
302
+ self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
303
+ self.mqtt_client.on_connect = self.on_mqtt_connect
304
+ self.mqtt_client.on_message = self.on_mqtt_message
305
+ try:
306
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
307
+ except Exception:
308
+ pass
309
+ try:
310
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
311
+ except Exception:
312
+ pass
305
313
 
306
314
  for cam in Component.objects.filter(
307
315
  controller_uid=IPCamera.uid
@@ -315,9 +323,11 @@ class GenericGatewayHandler(
315
323
  ).start()
316
324
 
317
325
  print("GATEWAY STARTED!")
326
+ self.mqtt_client.loop_start()
318
327
  while not exit.is_set():
319
- mqtt_client.loop()
320
- mqtt_client.disconnect()
328
+ time.sleep(1)
329
+ self.mqtt_client.loop_stop()
330
+ self.mqtt_client.disconnect()
321
331
 
322
332
 
323
333
  def on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
@@ -425,8 +435,14 @@ class GenericGatewayHandler(
425
435
  base_type='binary-sensor', alarm_category='security'
426
436
  ):
427
437
  if sensor.id not in self.sensors_on_watch[state.id]:
428
- self.sensors_on_watch[state.id][sensor.id] = i_id
429
- sensor.on_change(self.security_sensor_change)
438
+ # Register callback only when MQTT subscription succeeds
439
+ try:
440
+ sensor.on_change(self.security_sensor_change)
441
+ except Exception:
442
+ # Leave it untracked so we retry on next tick
443
+ raise
444
+ else:
445
+ self.sensors_on_watch[state.id][sensor.id] = i_id
430
446
 
431
447
  if state.controller._check_is_away(self.last_sensor_actions.get(i_id, 0)):
432
448
  if state.value != 'away':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simo
3
- Version: 3.1.5
3
+ Version: 3.1.7
4
4
  Summary: Smart Home Supremacy
5
5
  Author-email: "Simon V." <simon@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=jtoG91nYlUPW-Pkl025bBoGELWFm8SQD3duTPsjRhfk,12230
23
23
  simo/automation/forms.py,sha256=NAQUS9qrXp2d4GuvdpVyWj0Yh7vqMyX6pzs5oX4ze5Y,9800
24
- simo/automation/gateways.py,sha256=SGq91nr2K1i8aSFh0XTZDHBb7TzFU_X24-wkt8IQkeM,17406
24
+ simo/automation/gateways.py,sha256=WhZNliJhpuf8FnHGqIaWXzW3qcac5AtqpU9THPYYgw4,17770
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=Pg-hMaASQPB5_BTAMkfqM6z4jdHWH8xMYWOvDxIvmx8,2126
@@ -102,13 +102,13 @@ simo/core/auto_urls.py,sha256=fM9Tqzt0OfJ2FNnePGp7LcbJAWzgEwaNAJy7FNXHY-o,1299
102
102
  simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
103
103
  simo/core/base_types.py,sha256=FNIS9Y7wmdbVl-dISLdSBYvMEiV4zSLpBOBDYOVyam0,6580
104
104
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
105
- simo/core/controllers.py,sha256=jGn9bDvUuAbFL_uCAV8aqXgDJz9akYDroJuyU0ONsC8,48563
105
+ simo/core/controllers.py,sha256=uEC4hSeSpyHLZjojIdMPrzy2-JiK9DkMmCZpE3BcK-I,48651
106
106
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
107
- simo/core/events.py,sha256=GQwolnqO4ZoxgyVi9C4eKXpMULD3gMstGJsxWhc1TWU,6172
107
+ simo/core/events.py,sha256=mjrYsByw0hGct4rXoLmnbBodViN8S1ReBIZG3g660Yo,6735
108
108
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
109
109
  simo/core/form_fields.py,sha256=b4wZ4n7OO0m0_BPPS9ILVrwBvhhjUB079YrroveFUWA,5222
110
- simo/core/forms.py,sha256=IwuljCxwqPcSn3eXJRF2pWjktvlQ4DH2LCsncHzCHw0,22720
111
- simo/core/gateways.py,sha256=ik9C9L0Z_pTuWXMDXCW09MuTgb6IGdMPn8uVQAJVw5I,4377
110
+ simo/core/forms.py,sha256=koGOIVt3gE1eejpjHFjAH-QZ1Zm2Woy51F5txh_AJVI,26191
111
+ simo/core/gateways.py,sha256=cM_du3VsHbSYUSWL5JHxXMV8yn6s-QrSzB3WreglGjw,4736
112
112
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
113
113
  simo/core/managers.py,sha256=Ampwe5K7gfE6IJULNCV35V8ysmMOdS_wz7mRzfaLZUw,3014
114
114
  simo/core/mcp.py,sha256=MDx_m6BmkYDxCrfFegchz6NMCDCB0Mrbjx4gb2iJxHU,5188
@@ -296,7 +296,7 @@ simo/core/management/_hub_template/hub/__pycache__/settings.cpython-312.pyc,sha2
296
296
  simo/core/management/_hub_template/hub/__pycache__/urls.cpython-312.pyc,sha256=dNFa6cLiHRIB5Efyaxd9SmC8hPbMkpvp8zvtkiwt1hc,178
297
297
  simo/core/management/_hub_template/hub/__pycache__/wsgi.cpython-312.pyc,sha256=oA9duba-bsc_OOQnUaz_qiaB-0OcsS5dfjSVHTscrUM,529
298
298
  simo/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
299
- simo/core/management/commands/gateways_manager.py,sha256=oHzgC-eV4w_KHkiz6eCAlt3XMIwtS8_7mHDQ4UsU5y0,6967
299
+ simo/core/management/commands/gateways_manager.py,sha256=_PlFQ40rtqML38RC0U_KFMbqHma-ACNgEQ_iUwvwUBs,7276
300
300
  simo/core/management/commands/on_http_start.py,sha256=kxtWB3lOSyQA8tVmZRmLmaoVsliy5mhZp9_wwCvkj_A,6279
301
301
  simo/core/management/commands/republish_mqtt_state.py,sha256=mc8e7qnhDyC9_fiYr6d0g2s_3wGUXCrLo95-HBIrkOA,2248
302
302
  simo/core/management/commands/run_app_mqtt_control.py,sha256=-eDDAXbGtjqN1OK7Ym2FIdP1wVNJYR0DdvIyRzmXPDc,5592
@@ -10675,7 +10675,7 @@ simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,8
10675
10675
  simo/generic/base_types.py,sha256=gJUJYpd_gE-f1ogzagAPA1u2TYljhyU0_SMlgGUvCVk,2318
10676
10676
  simo/generic/controllers.py,sha256=oQ7mLYU-MxO4lpCQEGJA0ClIw253Ayzx3pOAFbkqC_4,53273
10677
10677
  simo/generic/forms.py,sha256=0RIDtLLzCkiSb9OxlioicOQW9yp1OjLKekpjbxzGVfM,26272
10678
- simo/generic/gateways.py,sha256=mnr4nAwLLESI50cwz1-LeeaLt0ABOt8s0D2_U-NgRsc,21586
10678
+ simo/generic/gateways.py,sha256=HWLwr9hmZPNbMRFiKtsjYhKxLPHlTqoIUBCmFD0JB08,22234
10679
10679
  simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
10680
10680
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10681
10681
  simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
@@ -11034,9 +11034,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
11034
11034
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11035
11035
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11036
11036
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11037
- simo-3.1.5.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
- simo-3.1.5.dist-info/METADATA,sha256=hXkJ2qxHLFHebhXd3yRy4Gvj2Bd4Vi2-ulumfeyYcIM,2224
11039
- simo-3.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
- simo-3.1.5.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
- simo-3.1.5.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
- simo-3.1.5.dist-info/RECORD,,
11037
+ simo-3.1.7.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
+ simo-3.1.7.dist-info/METADATA,sha256=478oQ_ahEy-rp4E2VvKgkzhLPFYrTOMG7IJAttlmJXM,2224
11039
+ simo-3.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
+ simo-3.1.7.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
+ simo-3.1.7.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
+ simo-3.1.7.dist-info/RECORD,,
File without changes