service-capacity-modeling 0.3.106__py3-none-any.whl → 0.3.108__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ """Plan comparison utilities and types for capacity planning.
2
+
3
+ This module provides functionality to compare two capacity plans and determine
4
+ if they are roughly equivalent, with detailed explanations of any differences.
5
+
6
+ Example usage::
7
+
8
+ from service_capacity_modeling.models.plan_comparison import (
9
+ compare_plans,
10
+ ignore_resource,
11
+ lte,
12
+ plus_or_minus,
13
+ ResourceTolerances,
14
+ )
15
+
16
+ # Get recommendation from planner
17
+ recommendations = planner.plan_certain(
18
+ model_name="org.netflix.cassandra",
19
+ region="us-east-1",
20
+ desires=desires,
21
+ )
22
+ recommendation = recommendations[0]
23
+
24
+ # Get current deployment as baseline using CapacityPlanner
25
+ # This uses model-specific cost methods for accurate comparison
26
+ baseline = planner.extract_baseline_plan(
27
+ model_name="org.netflix.cassandra",
28
+ region="us-east-1",
29
+ desires=desires, # must have current_clusters populated
30
+ extra_model_arguments={"copies_per_region": 3},
31
+ )
32
+
33
+ # Compare with custom tolerances
34
+ result = compare_plans(
35
+ baseline,
36
+ recommendation,
37
+ tolerances=ResourceTolerances(
38
+ annual_cost=ignore_resource(), # Don't care about cost
39
+ cpu=lte(1.05), # CPU can be at most 5% over baseline
40
+ disk=plus_or_minus(0.10), # Storage within ±10%
41
+ ),
42
+ )
43
+
44
+ if result.is_equivalent:
45
+ print("Current capacity is sufficient")
46
+ else:
47
+ print("Capacity adjustments needed:")
48
+ for diff in result.get_out_of_tolerance():
49
+ print(f" - {diff}")
50
+ """
51
+
52
+ from functools import lru_cache
53
+ from itertools import chain
54
+
55
+ from pydantic import ConfigDict
56
+
57
+ from service_capacity_modeling.enum_utils import enum_docstrings
58
+ from service_capacity_modeling.enum_utils import StrEnum
59
+
60
+ from service_capacity_modeling.interface import (
61
+ CapacityPlan,
62
+ default_reference_shape,
63
+ ExcludeUnsetModel,
64
+ Instance,
65
+ )
66
+
67
+
68
+ @enum_docstrings
69
+ class ResourceType(StrEnum):
70
+ """Resource types that can be compared between capacity plans."""
71
+
72
+ annual_cost = "annual_cost"
73
+ """Annual cost in dollars"""
74
+
75
+ cpu = "cpu"
76
+ """CPU cores required"""
77
+
78
+ mem_gib = "mem_gib"
79
+ """Memory in GiB"""
80
+
81
+ disk_gib = "disk_gib"
82
+ """Disk storage in GiB"""
83
+
84
+ network_mbps = "network_mbps"
85
+ """Network bandwidth in Mbps"""
86
+
87
+
88
+ # Tolerance for floating-point comparisons (e.g., exact_match)
89
+ _FLOAT_TOLERANCE = 1e-9
90
+
91
+
92
+ class Tolerance(ExcludeUnsetModel):
93
+ """Tolerance bounds for comparison / baseline ratio.
94
+
95
+ The bounds define the acceptable range for the ratio:
96
+ - ratio > 1.0 = comparison exceeds baseline (need more)
97
+ - ratio < 1.0 = baseline exceeds comparison (have extra capacity)
98
+ - ratio = 1.0 = exact match
99
+
100
+ Examples:
101
+ Tolerance(lower=0.0, upper=1.1) # lte(1.1) - ratio ≤ 1.1
102
+ Tolerance(lower=0.9, upper=float('inf')) # gte(0.9) - ratio ≥ 0.9
103
+ Tolerance(lower=0.9, upper=1.1) # tolerance(0.9, 1.1)
104
+ """
105
+
106
+ lower: float = 1.0
107
+ """Lower bound for ratio (< 1.0 means can have extra capacity)"""
108
+
109
+ upper: float = 1.0
110
+ """Upper bound for ratio (> 1.0 means requirement can exceed baseline)"""
111
+
112
+ model_config = ConfigDict(frozen=True)
113
+
114
+ def __contains__(self, ratio: float) -> bool:
115
+ """Check if a ratio is within tolerance bounds.
116
+
117
+ Args:
118
+ ratio: comparison / baseline
119
+
120
+ Returns:
121
+ True if within bounds (equivalent), False otherwise
122
+ """
123
+ return self.lower <= ratio <= self.upper
124
+
125
+
126
+ # -----------------------------------------------------------------------------
127
+ # Tolerance helper functions
128
+ # -----------------------------------------------------------------------------
129
+
130
+
131
+ @lru_cache(256)
132
+ def tolerance(lower: float, upper: float) -> Tolerance:
133
+ """Create a tolerance with explicit ratio bounds.
134
+
135
+ Args:
136
+ lower: Lower bound for ratio (e.g., 0.9 means comparison ≥ 0.9× baseline)
137
+ upper: Upper bound for ratio (e.g., 1.1 means comparison ≤ 1.1× baseline)
138
+
139
+ Returns:
140
+ Tolerance with specified bounds
141
+
142
+ Example:
143
+ >>> t = tolerance(0.9, 1.1) # Allow 0.9× to 1.1× baseline
144
+ >>> 0.95 in t # 0.95× baseline: True
145
+ >>> 1.05 in t # 1.05× baseline: True
146
+ >>> 1.15 in t # 1.15× baseline: False
147
+ """
148
+ return Tolerance(lower=lower, upper=upper)
149
+
150
+
151
+ def lte(upper: float) -> Tolerance:
152
+ """Ratio must be ≤ upper bound.
153
+
154
+ Use this to limit how much the requirement can exceed the baseline.
155
+ Any amount of extra capacity (ratio < 1.0) is acceptable.
156
+
157
+ Args:
158
+ upper: Upper bound as a ratio (e.g., 1.1 means comparison ≤ 1.1× baseline).
159
+
160
+ Example:
161
+ With baseline=100:
162
+
163
+ >>> tol = lte(1.1) # ratio must be ≤ 1.1
164
+
165
+ comparison=110 → ratio = 1.1 → OK (at boundary)
166
+ comparison=111 → ratio = 1.11 → NOT OK
167
+ comparison=90 → ratio = 0.9 → OK (extra capacity)
168
+ """
169
+ return tolerance(0.0, upper)
170
+
171
+
172
+ def gte(lower: float) -> Tolerance:
173
+ """Ratio must be ≥ lower bound.
174
+
175
+ Use this to limit how much extra capacity is acceptable.
176
+ Any amount of requirement exceeding baseline (ratio > 1.0) is acceptable.
177
+
178
+ Args:
179
+ lower: Lower bound as a ratio (e.g., 0.9 means comparison ≥ 0.9× baseline).
180
+
181
+ Example:
182
+ With baseline=100:
183
+
184
+ >>> tol = gte(0.9) # ratio must be ≥ 0.9
185
+
186
+ comparison=90 → ratio = 0.9 → OK (at boundary)
187
+ comparison=89 → ratio = 0.89 → NOT OK (too much extra)
188
+ comparison=110 → ratio = 1.1 → OK (need more)
189
+ """
190
+ return tolerance(lower, float("inf"))
191
+
192
+
193
+ def plus_or_minus(percent: float) -> Tolerance:
194
+ """Create a symmetric tolerance of ±percent around 1.0.
195
+
196
+ Args:
197
+ percent: The tolerance as a decimal (e.g., 0.10 for ±10%).
198
+
199
+ Example:
200
+ >>> tol = plus_or_minus(0.10) # ratio must be 0.9 to 1.1
201
+
202
+ comparison=110, baseline=100 → ratio = 1.1 → OK (at boundary)
203
+ comparison=89, baseline=100 → ratio = 0.89 → NOT OK
204
+ """
205
+ return tolerance(1.0 - percent, 1.0 + percent)
206
+
207
+
208
+ def exact_match() -> Tolerance:
209
+ """Create a tolerance requiring exact match (ratio = 1.0)."""
210
+ return tolerance(1.0 - _FLOAT_TOLERANCE, 1.0 + _FLOAT_TOLERANCE)
211
+
212
+
213
+ def ignore_resource() -> Tolerance:
214
+ """Create a tolerance that ignores a resource (any ratio acceptable)."""
215
+ return tolerance(0.0, float("inf"))
216
+
217
+
218
+ class ResourceTolerances(ExcludeUnsetModel):
219
+ """Per-resource tolerance configuration for plan comparison.
220
+
221
+ Set specific tolerances to override for individual resources.
222
+ Unset resources fall back to `default`.
223
+
224
+ Example:
225
+ >>> tolerances = ResourceTolerances(
226
+ ... default=lte(1.1), # comparison ≤ 1.1× baseline
227
+ ... cpu=lte(1.05), # Stricter for CPU
228
+ ... annual_cost=ignore_resource(), # Don't care about cost
229
+ ... )
230
+ """
231
+
232
+ default: Tolerance = plus_or_minus(0.10)
233
+ """Default tolerance: ±10% (ratio must be 0.9× to 1.1× baseline)"""
234
+
235
+ annual_cost: Tolerance | None = None
236
+ """Tolerance for annual cost comparison"""
237
+
238
+ cpu: Tolerance | None = None
239
+ """Tolerance for CPU cores comparison"""
240
+
241
+ memory: Tolerance | None = None
242
+ """Tolerance for memory (GiB) comparison"""
243
+
244
+ disk: Tolerance | None = None
245
+ """Tolerance for disk (GiB) comparison"""
246
+
247
+ network: Tolerance | None = None
248
+ """Tolerance for network (Mbps) comparison"""
249
+
250
+ def get_tolerance(self, resource: ResourceType) -> Tolerance:
251
+ """Get tolerance for a resource type, falling back to default."""
252
+ match resource:
253
+ case ResourceType.annual_cost:
254
+ return self.annual_cost or self.default
255
+ case ResourceType.cpu:
256
+ return self.cpu or self.default
257
+ case ResourceType.mem_gib:
258
+ return self.memory or self.default
259
+ case ResourceType.disk_gib:
260
+ return self.disk or self.default
261
+ case ResourceType.network_mbps:
262
+ return self.network or self.default
263
+ case _:
264
+ raise ValueError(f"Unknown resource type: {resource}")
265
+
266
+
267
+ class ResourceComparison(ExcludeUnsetModel):
268
+ """Represents a comparison of a resource between baseline and comparison plans.
269
+
270
+ Properties:
271
+ - is_equivalent: ratio is within tolerance bounds
272
+ - exceeds_upper_bound: ratio > upper bound (need more than tolerance allows)
273
+ - exceeds_lower_bound: ratio < lower bound (have more extra than tolerance allows)
274
+ """
275
+
276
+ resource: ResourceType
277
+ """Resource type being compared"""
278
+
279
+ baseline_value: float
280
+ """Value from the current deployment"""
281
+
282
+ comparison_value: float
283
+ """Value from the recommendation"""
284
+
285
+ tolerance: Tolerance
286
+ """The tolerance bounds that were applied for this comparison"""
287
+
288
+ @property
289
+ def ratio(self) -> float:
290
+ """Ratio: comparison / baseline.
291
+
292
+ - ratio > 1.0 = comparison exceeds baseline (need more)
293
+ - ratio < 1.0 = baseline exceeds comparison (have extra capacity)
294
+ - ratio = 1.0 = exact match
295
+
296
+ Examples:
297
+ - Baseline=100, Comparison=110 → ratio = 1.1 (need 10% more)
298
+ - Baseline=100, Comparison=90 → ratio = 0.9 (have 10% extra)
299
+ """
300
+ if self.baseline_value == 0:
301
+ if self.comparison_value == 0:
302
+ return 1.0 # Both zero = exact match
303
+ return float("inf") # comparison > 0, baseline = 0
304
+ return self.comparison_value / self.baseline_value
305
+
306
+ @property
307
+ def is_equivalent(self) -> bool:
308
+ """True if the ratio is within tolerance bounds."""
309
+ return self.ratio in self.tolerance
310
+
311
+ @property
312
+ def exceeds_lower_bound(self) -> bool:
313
+ """True if ratio < lower bound (too much extra capacity)."""
314
+ return self.ratio < self.tolerance.lower
315
+
316
+ @property
317
+ def exceeds_upper_bound(self) -> bool:
318
+ """True if ratio > upper bound (requirement exceeds tolerance)."""
319
+ return self.ratio > self.tolerance.upper
320
+
321
+ def __str__(self) -> str:
322
+ """Human-readable explanation of the comparison."""
323
+ if self.tolerance.upper == float("inf"):
324
+ bounds_str = f"≥ {self.tolerance.lower:.2f}×"
325
+ elif self.tolerance.lower == 0.0:
326
+ bounds_str = f"≤ {self.tolerance.upper:.2f}×"
327
+ else:
328
+ bounds_str = f"{self.tolerance.lower:.2f}× to {self.tolerance.upper:.2f}×"
329
+
330
+ if self.is_equivalent:
331
+ return (
332
+ f"{self.resource.value}: {self.ratio:.2f}× "
333
+ f"(within tolerance: {bounds_str})"
334
+ )
335
+ else:
336
+ bound = "lower" if self.exceeds_lower_bound else "upper"
337
+ return (
338
+ f"{self.resource.value}: exceeds {bound} bound, "
339
+ f"ratio={self.ratio:.2f}× (baseline={self.baseline_value:.2f}, "
340
+ f"comparison={self.comparison_value:.2f}, tolerance: {bounds_str})"
341
+ )
342
+
343
+
344
+ class PlanComparisonResult(ExcludeUnsetModel):
345
+ """Result of comparing two capacity plans for equivalence."""
346
+
347
+ is_equivalent: bool
348
+ """True if plans are within tolerance, False if significant differences"""
349
+
350
+ comparisons: dict[ResourceType, ResourceComparison] = {}
351
+ """Resource comparisons keyed by resource type"""
352
+
353
+ @property
354
+ def cpu(self) -> ResourceComparison:
355
+ """Get CPU comparison result."""
356
+ return self.comparisons[ResourceType.cpu]
357
+
358
+ @property
359
+ def memory(self) -> ResourceComparison:
360
+ """Get memory comparison result."""
361
+ return self.comparisons[ResourceType.mem_gib]
362
+
363
+ @property
364
+ def disk(self) -> ResourceComparison:
365
+ """Get disk comparison result."""
366
+ return self.comparisons[ResourceType.disk_gib]
367
+
368
+ @property
369
+ def network(self) -> ResourceComparison:
370
+ """Get network comparison result."""
371
+ return self.comparisons[ResourceType.network_mbps]
372
+
373
+ @property
374
+ def annual_cost(self) -> ResourceComparison:
375
+ """Get annual cost comparison result."""
376
+ return self.comparisons[ResourceType.annual_cost]
377
+
378
+ def get_out_of_tolerance(self) -> list[ResourceComparison]:
379
+ """Get only comparisons that exceed tolerance bounds."""
380
+ return [c for c in self.comparisons.values() if not c.is_equivalent]
381
+
382
+
383
+ def to_reference_cores(core_count: float, instance: Instance) -> float:
384
+ """Convert instance cores to reference-equivalent cores.
385
+
386
+ This is the inverse of normalize_cores() from models.common. While
387
+ normalize_cores answers "how many target cores to match N reference cores",
388
+ this answers "how many reference cores is N instance cores equivalent to".
389
+
390
+ Mathematically equivalent to:
391
+ normalize_cores(core_count, target=default_reference_shape, reference=instance)
392
+
393
+ but returns float instead of ceiling int. We need float precision for
394
+ accurate ratio comparisons - ceiling would distort ratios by up to ~3%.
395
+
396
+ See normalize_cores() in models/common.py for the original implementation.
397
+
398
+ Args:
399
+ core_count: Number of cores on the instance
400
+ instance: The instance shape (with cpu_ghz and cpu_ipc_scale)
401
+
402
+ Returns:
403
+ Equivalent cores on default_reference_shape (2.3 GHz, IPC 1.0)
404
+
405
+ Example:
406
+ # 32 cores on a 2.4 GHz instance = 33.4 reference cores
407
+ to_reference_cores(32, instance_at_2_4_ghz) # → 33.39
408
+ """
409
+ instance_speed = instance.cpu_ghz * instance.cpu_ipc_scale
410
+ ref_speed = default_reference_shape.cpu_ghz * default_reference_shape.cpu_ipc_scale
411
+ return core_count * (instance_speed / ref_speed)
412
+
413
+
414
+ def _aggregate_resources(plan: CapacityPlan) -> dict[ResourceType, float]:
415
+ """Aggregate resource values from a plan, normalizing CPU to reference shape.
416
+
417
+ CPU is computed from candidate_clusters and normalized to default_reference_shape
418
+ using IPC and frequency factors. This ensures consistent comparison even when
419
+ plans use different instance types with varying CPU performance characteristics.
420
+
421
+ Memory, disk, and network are summed from requirements (no normalization needed).
422
+ """
423
+ totals: dict[ResourceType, float] = {
424
+ ResourceType.cpu: 0.0,
425
+ ResourceType.mem_gib: 0.0,
426
+ ResourceType.disk_gib: 0.0,
427
+ ResourceType.network_mbps: 0.0,
428
+ }
429
+
430
+ # CPU: compute from candidate_clusters, normalized to reference shape
431
+ for cluster in chain(
432
+ plan.candidate_clusters.zonal, plan.candidate_clusters.regional
433
+ ):
434
+ totals[ResourceType.cpu] += to_reference_cores(
435
+ cluster.instance.cpu * cluster.count, cluster.instance
436
+ )
437
+
438
+ # Other resources: sum from requirements
439
+ for req in chain(plan.requirements.zonal, plan.requirements.regional):
440
+ totals[ResourceType.mem_gib] += req.mem_gib.mid
441
+ totals[ResourceType.disk_gib] += req.disk_gib.mid
442
+ totals[ResourceType.network_mbps] += req.network_mbps.mid
443
+
444
+ return totals
445
+
446
+
447
+ def compare_plans(
448
+ baseline: CapacityPlan,
449
+ comparison: CapacityPlan,
450
+ tolerances: ResourceTolerances | None = None,
451
+ ) -> PlanComparisonResult:
452
+ """Compare two capacity plans to determine if they are roughly equivalent.
453
+
454
+ This function compares plans across multiple dimensions (cost, CPU, memory,
455
+ disk, network) and determines if the differences are significant based on
456
+ the provided tolerance bounds.
457
+
458
+ Args:
459
+ baseline: The reference plan (e.g., current production deployment)
460
+ comparison: The plan to compare against baseline (e.g., new recommendation)
461
+ tolerances: Per-resource tolerance configuration. If None, uses defaults.
462
+
463
+ Returns:
464
+ PlanComparisonResult containing:
465
+ - is_equivalent: True if all resources within tolerance, False otherwise
466
+ - differences: Dict of ResourceDifference for ALL resources (use
467
+ get_out_of_tolerance() to filter to only problematic ones)
468
+
469
+ Example:
470
+ >>> result = compare_plans(baseline, recommended)
471
+ >>> if result.cpu.exceeds_lower_bound:
472
+ ... print("Current has excess CPU capacity")
473
+ >>> for diff in result.get_out_of_tolerance():
474
+ ... print(diff) # Human-readable explanation
475
+ """
476
+ if tolerances is None:
477
+ tolerances = ResourceTolerances()
478
+
479
+ baseline_cost = float(baseline.candidate_clusters.total_annual_cost)
480
+ comparison_cost = float(comparison.candidate_clusters.total_annual_cost)
481
+ baseline_resources = _aggregate_resources(baseline)
482
+ comparison_resources = _aggregate_resources(comparison)
483
+
484
+ def make_comparison(
485
+ resource: ResourceType, baseline_val: float, comparison_val: float
486
+ ) -> ResourceComparison:
487
+ return ResourceComparison(
488
+ resource=resource,
489
+ baseline_value=baseline_val,
490
+ comparison_value=comparison_val,
491
+ tolerance=tolerances.get_tolerance(resource),
492
+ )
493
+
494
+ comparisons = {
495
+ ResourceType.annual_cost: make_comparison(
496
+ ResourceType.annual_cost, baseline_cost, comparison_cost
497
+ ),
498
+ ResourceType.cpu: make_comparison(
499
+ ResourceType.cpu,
500
+ baseline_resources[ResourceType.cpu],
501
+ comparison_resources[ResourceType.cpu],
502
+ ),
503
+ ResourceType.mem_gib: make_comparison(
504
+ ResourceType.mem_gib,
505
+ baseline_resources[ResourceType.mem_gib],
506
+ comparison_resources[ResourceType.mem_gib],
507
+ ),
508
+ ResourceType.disk_gib: make_comparison(
509
+ ResourceType.disk_gib,
510
+ baseline_resources[ResourceType.disk_gib],
511
+ comparison_resources[ResourceType.disk_gib],
512
+ ),
513
+ ResourceType.network_mbps: make_comparison(
514
+ ResourceType.network_mbps,
515
+ baseline_resources[ResourceType.network_mbps],
516
+ comparison_resources[ResourceType.network_mbps],
517
+ ),
518
+ }
519
+
520
+ return PlanComparisonResult(
521
+ is_equivalent=all(c.is_equivalent for c in comparisons.values()),
522
+ comparisons=comparisons,
523
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: service-capacity-modeling
3
- Version: 0.3.106
3
+ Version: 0.3.108
4
4
  Summary: Contains utilities for modeling capacity for pluggable workloads
5
5
  Author: Joseph Lynch
6
6
  Author-email: josephl@netflix.com
@@ -54,6 +54,7 @@ service_capacity_modeling/hardware/profiles/shapes/aws/manual_services.json,sha2
54
54
  service_capacity_modeling/models/__init__.py,sha256=MbnmdVfxDJFVtS5d6GK567RSa5V086oEDX-tJtB68WA,15494
55
55
  service_capacity_modeling/models/common.py,sha256=6a2ar_0lrrXKZtBAFKl7ETmd2Vp7lneH2C9eXUy0TBM,37713
56
56
  service_capacity_modeling/models/headroom_strategy.py,sha256=rGo_d7nxkQDjx0_hIAXKKZAWnQDBtqZhc0eTMouVh8s,682
57
+ service_capacity_modeling/models/plan_comparison.py,sha256=3Ed4hcB6dVOmkUFtPFLJokW-b4TmI80QgvxbXItvmWQ,18096
57
58
  service_capacity_modeling/models/utils.py,sha256=WlBaU9l11V5atThMTWDV9-FT1uf0FJS_2iyu8RF5NIk,2665
58
59
  service_capacity_modeling/models/org/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
60
  service_capacity_modeling/models/org/netflix/__init__.py,sha256=keaBt7dk6DB2VuRINdo8wRfsobK655Gfw3hYjruacJs,2553
@@ -84,9 +85,9 @@ service_capacity_modeling/tools/fetch_pricing.py,sha256=fO84h77cqiiIHF4hZt490Rwb
84
85
  service_capacity_modeling/tools/generate_missing.py,sha256=F7YqvMJAV4nZc20GNrlIsnQSF8_77sLgwYZqc5k4LDg,3099
85
86
  service_capacity_modeling/tools/instance_families.py,sha256=e5RuYkCLUITvsAazDH12B6KjX_PaBsv6Ne3mj0HK_sQ,9223
86
87
  service_capacity_modeling/tools/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- service_capacity_modeling-0.3.106.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
88
- service_capacity_modeling-0.3.106.dist-info/METADATA,sha256=_SvhiphVKCZqoHRPWFVckzh0W7xhSLNz04focrlzDLw,10367
89
- service_capacity_modeling-0.3.106.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
90
- service_capacity_modeling-0.3.106.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
91
- service_capacity_modeling-0.3.106.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
92
- service_capacity_modeling-0.3.106.dist-info/RECORD,,
88
+ service_capacity_modeling-0.3.108.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
89
+ service_capacity_modeling-0.3.108.dist-info/METADATA,sha256=wF3U0J15kzsj0ZjgN5-7DjaM3qKxRZWmAq5w_Ip8_1k,10367
90
+ service_capacity_modeling-0.3.108.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
91
+ service_capacity_modeling-0.3.108.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
92
+ service_capacity_modeling-0.3.108.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
93
+ service_capacity_modeling-0.3.108.dist-info/RECORD,,