margin 0.9.4__tar.gz → 0.9.5__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.

Potentially problematic release.


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

Files changed (85) hide show
  1. {margin-0.9.4/margin.egg-info → margin-0.9.5}/PKG-INFO +1 -1
  2. {margin-0.9.4 → margin-0.9.5}/margin/__init__.py +2 -1
  3. {margin-0.9.4 → margin-0.9.5}/margin/calibrate.py +5 -5
  4. {margin-0.9.4 → margin-0.9.5}/margin/contract.py +32 -0
  5. {margin-0.9.4 → margin-0.9.5}/margin/loop.py +3 -1
  6. {margin-0.9.4 → margin-0.9.5/margin.egg-info}/PKG-INFO +1 -1
  7. {margin-0.9.4 → margin-0.9.5}/pyproject.toml +1 -1
  8. {margin-0.9.4 → margin-0.9.5}/tests/test_calibrate.py +43 -0
  9. {margin-0.9.4 → margin-0.9.5}/tests/test_contract.py +50 -0
  10. {margin-0.9.4 → margin-0.9.5}/tests/test_policy_trace.py +40 -17
  11. {margin-0.9.4 → margin-0.9.5}/LICENSE +0 -0
  12. {margin-0.9.4 → margin-0.9.5}/NOTICE +0 -0
  13. {margin-0.9.4 → margin-0.9.5}/README.md +0 -0
  14. {margin-0.9.4 → margin-0.9.5}/margin/__main__.py +0 -0
  15. {margin-0.9.4 → margin-0.9.5}/margin/algebra.py +0 -0
  16. {margin-0.9.4 → margin-0.9.5}/margin/anomaly.py +0 -0
  17. {margin-0.9.4 → margin-0.9.5}/margin/bridge.py +0 -0
  18. {margin-0.9.4 → margin-0.9.5}/margin/causal.py +0 -0
  19. {margin-0.9.4 → margin-0.9.5}/margin/composite.py +0 -0
  20. {margin-0.9.4 → margin-0.9.5}/margin/confidence.py +0 -0
  21. {margin-0.9.4 → margin-0.9.5}/margin/config.py +0 -0
  22. {margin-0.9.4 → margin-0.9.5}/margin/correlate.py +0 -0
  23. {margin-0.9.4 → margin-0.9.5}/margin/diff.py +0 -0
  24. {margin-0.9.4 → margin-0.9.5}/margin/drift.py +0 -0
  25. {margin-0.9.4 → margin-0.9.5}/margin/events.py +0 -0
  26. {margin-0.9.4 → margin-0.9.5}/margin/forecast.py +0 -0
  27. {margin-0.9.4 → margin-0.9.5}/margin/health.py +0 -0
  28. {margin-0.9.4 → margin-0.9.5}/margin/intent.py +0 -0
  29. {margin-0.9.4 → margin-0.9.5}/margin/ledger.py +0 -0
  30. {margin-0.9.4 → margin-0.9.5}/margin/observation.py +0 -0
  31. {margin-0.9.4 → margin-0.9.5}/margin/persist.py +0 -0
  32. {margin-0.9.4 → margin-0.9.5}/margin/policy/__init__.py +0 -0
  33. {margin-0.9.4 → margin-0.9.5}/margin/policy/compose.py +0 -0
  34. {margin-0.9.4 → margin-0.9.5}/margin/policy/core.py +0 -0
  35. {margin-0.9.4 → margin-0.9.5}/margin/policy/temporal.py +0 -0
  36. {margin-0.9.4 → margin-0.9.5}/margin/policy/trace.py +0 -0
  37. {margin-0.9.4 → margin-0.9.5}/margin/policy/tuning.py +0 -0
  38. {margin-0.9.4 → margin-0.9.5}/margin/policy/validate.py +0 -0
  39. {margin-0.9.4 → margin-0.9.5}/margin/predicates.py +0 -0
  40. {margin-0.9.4 → margin-0.9.5}/margin/provenance.py +0 -0
  41. {margin-0.9.4 → margin-0.9.5}/margin/streaming.py +0 -0
  42. {margin-0.9.4 → margin-0.9.5}/margin/transitions.py +0 -0
  43. {margin-0.9.4 → margin-0.9.5}/margin/uncertain.py +0 -0
  44. {margin-0.9.4 → margin-0.9.5}/margin/validity.py +0 -0
  45. {margin-0.9.4 → margin-0.9.5}/margin.egg-info/SOURCES.txt +0 -0
  46. {margin-0.9.4 → margin-0.9.5}/margin.egg-info/dependency_links.txt +0 -0
  47. {margin-0.9.4 → margin-0.9.5}/margin.egg-info/top_level.txt +0 -0
  48. {margin-0.9.4 → margin-0.9.5}/setup.cfg +0 -0
  49. {margin-0.9.4 → margin-0.9.5}/tests/test_algebra.py +0 -0
  50. {margin-0.9.4 → margin-0.9.5}/tests/test_anomaly.py +0 -0
  51. {margin-0.9.4 → margin-0.9.5}/tests/test_bridge.py +0 -0
  52. {margin-0.9.4 → margin-0.9.5}/tests/test_causal.py +0 -0
  53. {margin-0.9.4 → margin-0.9.5}/tests/test_composite.py +0 -0
  54. {margin-0.9.4 → margin-0.9.5}/tests/test_confidence.py +0 -0
  55. {margin-0.9.4 → margin-0.9.5}/tests/test_config.py +0 -0
  56. {margin-0.9.4 → margin-0.9.5}/tests/test_correlate.py +0 -0
  57. {margin-0.9.4 → margin-0.9.5}/tests/test_diff.py +0 -0
  58. {margin-0.9.4 → margin-0.9.5}/tests/test_drift.py +0 -0
  59. {margin-0.9.4 → margin-0.9.5}/tests/test_events.py +0 -0
  60. {margin-0.9.4 → margin-0.9.5}/tests/test_forecast.py +0 -0
  61. {margin-0.9.4 → margin-0.9.5}/tests/test_health.py +0 -0
  62. {margin-0.9.4 → margin-0.9.5}/tests/test_healthcare.py +0 -0
  63. {margin-0.9.4 → margin-0.9.5}/tests/test_intent.py +0 -0
  64. {margin-0.9.4 → margin-0.9.5}/tests/test_ledger.py +0 -0
  65. {margin-0.9.4 → margin-0.9.5}/tests/test_loop.py +0 -0
  66. {margin-0.9.4 → margin-0.9.5}/tests/test_neuro.py +0 -0
  67. {margin-0.9.4 → margin-0.9.5}/tests/test_observation.py +0 -0
  68. {margin-0.9.4 → margin-0.9.5}/tests/test_persist.py +0 -0
  69. {margin-0.9.4 → margin-0.9.5}/tests/test_policy.py +0 -0
  70. {margin-0.9.4 → margin-0.9.5}/tests/test_policy_compose.py +0 -0
  71. {margin-0.9.4 → margin-0.9.5}/tests/test_policy_temporal.py +0 -0
  72. {margin-0.9.4 → margin-0.9.5}/tests/test_policy_tuning.py +0 -0
  73. {margin-0.9.4 → margin-0.9.5}/tests/test_policy_validate.py +0 -0
  74. {margin-0.9.4 → margin-0.9.5}/tests/test_predicates.py +0 -0
  75. {margin-0.9.4 → margin-0.9.5}/tests/test_printer3d.py +0 -0
  76. {margin-0.9.4 → margin-0.9.5}/tests/test_provenance.py +0 -0
  77. {margin-0.9.4 → margin-0.9.5}/tests/test_reverse_bridge.py +0 -0
  78. {margin-0.9.4 → margin-0.9.5}/tests/test_ros2.py +0 -0
  79. {margin-0.9.4 → margin-0.9.5}/tests/test_staleness.py +0 -0
  80. {margin-0.9.4 → margin-0.9.5}/tests/test_streaming.py +0 -0
  81. {margin-0.9.4 → margin-0.9.5}/tests/test_transformer.py +0 -0
  82. {margin-0.9.4 → margin-0.9.5}/tests/test_transitions.py +0 -0
  83. {margin-0.9.4 → margin-0.9.5}/tests/test_uncertain.py +0 -0
  84. {margin-0.9.4 → margin-0.9.5}/tests/test_validity.py +0 -0
  85. {margin-0.9.4 → margin-0.9.5}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -82,6 +82,7 @@ from .contract import (
82
82
  HealthTarget, ReachHealth, SustainHealth,
83
83
  RecoveryThreshold, NoHarmful,
84
84
  ContractResult, Contract,
85
+ contract_term_from_dict,
85
86
  )
86
87
 
87
88
  # Correlation
@@ -164,7 +165,7 @@ __all__ = [
164
165
  "TermStatus", "TermResult", "ContractTerm",
165
166
  "HealthTarget", "ReachHealth", "SustainHealth",
166
167
  "RecoveryThreshold", "NoHarmful",
167
- "ContractResult", "Contract",
168
+ "ContractResult", "Contract", "contract_term_from_dict",
168
169
  # Correlation
169
170
  "Correlation", "CorrelationMatrix",
170
171
  "correlate_pair", "correlate", "correlate_from_ledger",
@@ -312,7 +312,7 @@ def recalibrate_parser(
312
312
  components: Optional[list[str]] = None,
313
313
  intact_fraction: float = 0.70,
314
314
  ablated_fraction: float = 0.30,
315
- active_min: float = 0.05,
315
+ active_min: Optional[float] = None,
316
316
  use_std: bool = False,
317
317
  intact_std_multiplier: float = 1.5,
318
318
  ablated_std_multiplier: float = 3.0,
@@ -338,8 +338,8 @@ def recalibrate_parser(
338
338
  all components in new_healthy_data are recalibrated
339
339
  intact_fraction: fraction of baseline for intact threshold (fraction mode)
340
340
  ablated_fraction: fraction of baseline for ablated threshold (fraction mode)
341
- active_min: inherited from existing parser thresholds; pass only
342
- to override for recalibrated components
341
+ active_min: None (default) inherits from each component's existing
342
+ threshold; pass an explicit float to override
343
343
  use_std: if True, use std-based thresholds (see calibrate())
344
344
  intact_std_multiplier: std deviations for intact threshold (std mode)
345
345
  ablated_std_multiplier: std deviations for ablated threshold (std mode)
@@ -373,8 +373,8 @@ def recalibrate_parser(
373
373
  # Resolve polarity: explicit override → existing parser polarity → True
374
374
  existing_t = parser.component_thresholds.get(name) or parser.thresholds
375
375
  hib = polarities.get(name, existing_t.higher_is_better)
376
- # Inherit active_min from existing thresholds unless caller overrides
377
- component_active_min = existing_t.active_min if active_min == 0.05 else active_min
376
+ # Inherit active_min from existing thresholds unless caller provides an explicit override
377
+ component_active_min = existing_t.active_min if active_min is None else active_min
378
378
  result = calibrate(
379
379
  vals, hib, intact_fraction, ablated_fraction, component_active_min,
380
380
  use_std=use_std,
@@ -232,6 +232,38 @@ class NoHarmful(ContractTerm):
232
232
  return {"type": "no_harmful", "name": self.name, "over_steps": self.over_steps}
233
233
 
234
234
 
235
+ _TERM_TYPES: dict = {
236
+ "health_target": lambda d: HealthTarget(
237
+ name=d["name"], component=d["component"],
238
+ target=Health(d["target"]), or_better=d.get("or_better", True),
239
+ ),
240
+ "reach_health": lambda d: ReachHealth(
241
+ name=d["name"], component=d["component"],
242
+ target=Health(d["target"]), within_steps=d["within_steps"],
243
+ ),
244
+ "sustain_health": lambda d: SustainHealth(
245
+ name=d["name"], component=d["component"],
246
+ target=Health(d["target"]), for_steps=d["for_steps"],
247
+ or_better=d.get("or_better", True),
248
+ ),
249
+ "recovery_threshold": lambda d: RecoveryThreshold(
250
+ name=d["name"], min_recovery=d["min_recovery"], over_steps=d["over_steps"],
251
+ ),
252
+ "no_harmful": lambda d: NoHarmful(
253
+ name=d["name"], over_steps=d["over_steps"],
254
+ ),
255
+ }
256
+
257
+
258
+ def contract_term_from_dict(d: dict) -> ContractTerm:
259
+ """Deserialize a ContractTerm subclass from a dict produced by its to_dict()."""
260
+ term_type = d.get("type")
261
+ if term_type not in _TERM_TYPES:
262
+ raise ValueError(f"Unknown contract term type: {term_type!r}. "
263
+ f"Expected one of: {list(_TERM_TYPES)}")
264
+ return _TERM_TYPES[term_type](d)
265
+
266
+
235
267
  # -----------------------------------------------------------------------
236
268
  # Contract — a set of terms evaluated together
237
269
  # -----------------------------------------------------------------------
@@ -50,7 +50,9 @@ class StepResult:
50
50
  @property
51
51
  def acted(self) -> bool:
52
52
  """True if a real correction was produced (not NOOP, not escalation)."""
53
- return self.correction is not None and self.correction.is_active()
53
+ if self.correction is not None and self.correction.is_active():
54
+ return True
55
+ return any(c.is_active() for c in self.corrections)
54
56
 
55
57
  @property
56
58
  def escalated(self) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.9.4"
7
+ version = "0.9.5"
8
8
  description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -208,3 +208,46 @@ class TestCalibrateManyReturnParser:
208
208
  import pytest
209
209
  with pytest.raises(ValueError):
210
210
  calibrate_many({}, return_parser=True)
211
+
212
+
213
+ class TestRecalibrateParserActiveMin:
214
+ def test_none_inherits_from_existing_parser(self):
215
+ from margin.calibrate import recalibrate_parser
216
+ # Parser with non-default active_min
217
+ parser = parser_from_calibration({"cpu": [100.0] * 5})
218
+ # Manually set a custom active_min on the thresholds
219
+ import dataclasses
220
+ custom_thresh = dataclasses.replace(parser.thresholds, active_min=0.15)
221
+ parser2 = parser.__class__(
222
+ baselines=parser.baselines,
223
+ thresholds=custom_thresh,
224
+ component_thresholds=parser.component_thresholds,
225
+ )
226
+ new_parser, _ = recalibrate_parser(parser2, {"cpu": [110.0] * 5})
227
+ # active_min should be inherited as 0.15, not the old 0.05 sentinel
228
+ result_thresh = new_parser.component_thresholds.get("cpu") or new_parser.thresholds
229
+ assert result_thresh.active_min == pytest.approx(0.15)
230
+
231
+ def test_explicit_active_min_overrides_existing(self):
232
+ from margin.calibrate import recalibrate_parser
233
+ parser = parser_from_calibration({"cpu": [100.0] * 5})
234
+ new_parser, _ = recalibrate_parser(parser, {"cpu": [110.0] * 5}, active_min=0.20)
235
+ result_thresh = new_parser.component_thresholds.get("cpu") or new_parser.thresholds
236
+ assert result_thresh.active_min == pytest.approx(0.20)
237
+
238
+ def test_explicit_005_is_not_swallowed(self):
239
+ # Regression: before fix, active_min=0.05 was sentinel for "inherit"
240
+ # so an explicit 0.05 would be silently replaced by existing parser's value
241
+ from margin.calibrate import recalibrate_parser
242
+ import dataclasses
243
+ parser = parser_from_calibration({"cpu": [100.0] * 5})
244
+ custom_thresh = dataclasses.replace(parser.thresholds, active_min=0.30)
245
+ parser2 = parser.__class__(
246
+ baselines=parser.baselines,
247
+ thresholds=custom_thresh,
248
+ component_thresholds=parser.component_thresholds,
249
+ )
250
+ # Explicitly pass 0.05 — should get 0.05, not 0.30
251
+ new_parser, _ = recalibrate_parser(parser2, {"cpu": [110.0] * 5}, active_min=0.05)
252
+ result_thresh = new_parser.component_thresholds.get("cpu") or new_parser.thresholds
253
+ assert result_thresh.active_min == pytest.approx(0.05)
@@ -222,3 +222,53 @@ class TestContract:
222
222
  d = r.to_dict()
223
223
  assert d["all_met"] is True
224
224
  assert d["n_met"] == 1
225
+
226
+
227
+ class TestContractTermFromDict:
228
+ def test_health_target_roundtrip(self):
229
+ from margin import contract_term_from_dict
230
+ from margin import HealthTarget
231
+ t = HealthTarget(name="t1", component="cpu", target=Health.INTACT)
232
+ t2 = contract_term_from_dict(t.to_dict())
233
+ assert isinstance(t2, HealthTarget)
234
+ assert t2.name == "t1"
235
+ assert t2.component == "cpu"
236
+ assert t2.target == Health.INTACT
237
+ assert t2.or_better is True
238
+
239
+ def test_reach_health_roundtrip(self):
240
+ from margin import contract_term_from_dict
241
+ from margin import ReachHealth
242
+ t = ReachHealth(name="r1", component="mem", target=Health.DEGRADED, within_steps=10)
243
+ t2 = contract_term_from_dict(t.to_dict())
244
+ assert isinstance(t2, ReachHealth)
245
+ assert t2.within_steps == 10
246
+
247
+ def test_sustain_health_roundtrip(self):
248
+ from margin import contract_term_from_dict
249
+ from margin import SustainHealth
250
+ t = SustainHealth(name="s1", component="cpu", target=Health.INTACT,
251
+ for_steps=5, or_better=False)
252
+ t2 = contract_term_from_dict(t.to_dict())
253
+ assert isinstance(t2, SustainHealth)
254
+ assert t2.for_steps == 5
255
+ assert t2.or_better is False
256
+
257
+ def test_recovery_threshold_roundtrip(self):
258
+ from margin import contract_term_from_dict, RecoveryThreshold
259
+ t = RecoveryThreshold(name="rt", min_recovery=0.8, over_steps=20)
260
+ t2 = contract_term_from_dict(t.to_dict())
261
+ assert isinstance(t2, RecoveryThreshold)
262
+ assert t2.min_recovery == pytest.approx(0.8)
263
+
264
+ def test_no_harmful_roundtrip(self):
265
+ from margin import contract_term_from_dict, NoHarmful
266
+ t = NoHarmful(name="nh", over_steps=15)
267
+ t2 = contract_term_from_dict(t.to_dict())
268
+ assert isinstance(t2, NoHarmful)
269
+ assert t2.over_steps == 15
270
+
271
+ def test_unknown_type_raises(self):
272
+ from margin import contract_term_from_dict
273
+ with pytest.raises(ValueError, match="Unknown"):
274
+ contract_term_from_dict({"type": "nonexistent", "name": "x"})
@@ -1,4 +1,5 @@
1
1
  import pytest
2
+ from margin.loop import step
2
3
  from margin.policy.core import Policy, PolicyRule, Action, Constraint, Escalation, EscalationLevel
3
4
  from margin.policy.trace import trace_evaluate, trace_backtest, DecisionTrace
4
5
  from margin.observation import Observation, Expression, Op
@@ -140,9 +141,6 @@ class TestBacktestProposedBy:
140
141
 
141
142
  class TestMultiRule:
142
143
  def _make_multi_policy(self):
143
- from margin.policy.core import Policy, PolicyRule, Action
144
- from margin.observation import Op
145
- from margin.predicates import any_health, any_degraded
146
144
  return Policy(
147
145
  name="multi",
148
146
  multi_rule=True,
@@ -169,9 +167,6 @@ class TestMultiRule:
169
167
  def test_trace_always_has_all_matched_results(self):
170
168
  # trace_evaluate always populates dt.results for all matches;
171
169
  # multi_rule only gates StepResult.corrections in step()
172
- from margin.policy.core import Policy, PolicyRule, Action
173
- from margin.observation import Op
174
- from margin.predicates import any_degraded
175
170
  policy = Policy(
176
171
  name="single",
177
172
  multi_rule=False,
@@ -191,7 +186,6 @@ class TestMultiRule:
191
186
  assert d["multi_rule"] is True
192
187
 
193
188
  def test_multi_rule_default_is_false(self):
194
- from margin.policy.core import Policy
195
189
  policy = Policy(name="p", rules=[])
196
190
  assert policy.multi_rule is False
197
191
  assert policy.to_dict()["multi_rule"] is False
@@ -199,11 +193,6 @@ class TestMultiRule:
199
193
 
200
194
  class TestStepResultCorrections:
201
195
  def test_step_corrections_populated_multi_rule(self):
202
- from margin.loop import step
203
- from margin.policy.core import Policy, PolicyRule, Action
204
- from margin.observation import Op
205
- from margin.predicates import any_degraded
206
-
207
196
  policy = Policy(
208
197
  name="multi",
209
198
  multi_rule=True,
@@ -216,11 +205,6 @@ class TestStepResultCorrections:
216
205
  assert len(result.corrections) >= 1
217
206
 
218
207
  def test_step_corrections_empty_single_rule(self):
219
- from margin.loop import step
220
- from margin.policy.core import Policy, PolicyRule, Action
221
- from margin.observation import Op
222
- from margin.predicates import any_degraded
223
-
224
208
  policy = Policy(
225
209
  name="single",
226
210
  multi_rule=False,
@@ -229,3 +213,42 @@ class TestStepResultCorrections:
229
213
  result = step(_expr(_obs("x", Health.DEGRADED)), policy)
230
214
  # multi_rule=False → corrections list is empty (only .correction populated)
231
215
  assert result.corrections == []
216
+
217
+
218
+ class TestStepResultActed:
219
+ def test_acted_true_when_correction_active(self):
220
+ policy = Policy(
221
+ name="p", multi_rule=False,
222
+ rules=[PolicyRule("r", any_degraded(), Action(target="*", op=Op.RESTORE, alpha=0.5))],
223
+ )
224
+ result = step(_expr(_obs("x", Health.DEGRADED)), policy)
225
+ assert result.acted is True
226
+
227
+ def test_acted_false_on_noop(self):
228
+ from margin.predicates import any_health
229
+ policy = Policy(
230
+ name="p", multi_rule=False,
231
+ rules=[PolicyRule("r", any_health(Health.DEGRADED), Action(target="*", op=Op.NOOP))],
232
+ )
233
+ result = step(_expr(_obs("x", Health.DEGRADED)), policy)
234
+ assert result.acted is False
235
+
236
+ def test_acted_true_when_correction_in_multi_rule_corrections_list(self):
237
+ # First result is Escalation (no min_alpha satisfied), second is a Correction
238
+ # multi_rule=True → both fire; acted should see the Correction in .corrections
239
+ policy = Policy(
240
+ name="p", multi_rule=True,
241
+ rules=[
242
+ PolicyRule("esc", any_degraded(),
243
+ Action(target="*", op=Op.RESTORE, alpha=0.1),
244
+ constraint=Constraint(min_alpha=0.9), # suppressed → escalation
245
+ priority=10),
246
+ PolicyRule("fix", any_degraded(),
247
+ Action(target="*", op=Op.RESTORE, alpha=0.5),
248
+ priority=5),
249
+ ],
250
+ )
251
+ result = step(_expr(_obs("x", Health.DEGRADED, 10.0)), policy)
252
+ # .corrections contains the Correction from "fix"
253
+ assert any(c.is_active() for c in result.corrections)
254
+ assert result.acted is True
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
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