logisticspy 0.1.0__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,43 @@
1
+ """
2
+ logisticspy: A Python toolkit for logistics and supply chain calculations.
3
+
4
+ The first module, ``logisticspy.weight``, calculates volumetric (dimensional)
5
+ and chargeable weight for air, courier, sea, road, and rail freight shipments.
6
+ More tools will be added over time.
7
+
8
+ Top-level convenience imports let you do either of:
9
+
10
+ import logisticspy
11
+ logisticspy.calculate(...)
12
+
13
+ from logisticspy.weight import chargeable_weight
14
+ chargeable_weight.calculate(...)
15
+ """
16
+
17
+ from .weight import (
18
+ calculate,
19
+ calculate_consignment,
20
+ WeightResult,
21
+ ConsolidatedResult,
22
+ Mode,
23
+ DEFAULT_DIVISORS,
24
+ DIVISOR_PRESETS,
25
+ UnsupportedUnitError,
26
+ UnsupportedModeError,
27
+ UnsupportedPresetError,
28
+ )
29
+
30
+ __version__ = "0.1.0"
31
+
32
+ __all__ = [
33
+ "calculate",
34
+ "calculate_consignment",
35
+ "WeightResult",
36
+ "ConsolidatedResult",
37
+ "Mode",
38
+ "DEFAULT_DIVISORS",
39
+ "DIVISOR_PRESETS",
40
+ "UnsupportedUnitError",
41
+ "UnsupportedModeError",
42
+ "UnsupportedPresetError",
43
+ ]
@@ -0,0 +1,28 @@
1
+ """
2
+ logisticspy.weight: Calculate volumetric (dimensional) and chargeable weight
3
+ for air, courier, sea, road, and rail freight shipments.
4
+ """
5
+
6
+ from .chargeable_weight import (
7
+ calculate,
8
+ calculate_consignment,
9
+ WeightResult,
10
+ ConsolidatedResult,
11
+ UnsupportedUnitError,
12
+ UnsupportedModeError,
13
+ UnsupportedPresetError,
14
+ )
15
+ from .divisors import Mode, DEFAULT_DIVISORS, DIVISOR_PRESETS
16
+
17
+ __all__ = [
18
+ "calculate",
19
+ "calculate_consignment",
20
+ "WeightResult",
21
+ "ConsolidatedResult",
22
+ "Mode",
23
+ "DEFAULT_DIVISORS",
24
+ "DIVISOR_PRESETS",
25
+ "UnsupportedUnitError",
26
+ "UnsupportedModeError",
27
+ "UnsupportedPresetError",
28
+ ]
@@ -0,0 +1,291 @@
1
+ """
2
+ Core calculation logic for volumetric (dimensional) and chargeable weight.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional, List
7
+
8
+ from .divisors import (
9
+ Mode,
10
+ DEFAULT_DIVISORS,
11
+ DIVISOR_PRESETS,
12
+ SEA_CBM_TO_KG,
13
+ LENGTH_TO_CM,
14
+ WEIGHT_TO_KG,
15
+ )
16
+
17
+
18
+ class UnsupportedUnitError(ValueError):
19
+ """Raised when an unrecognized unit is supplied."""
20
+
21
+
22
+ class UnsupportedModeError(ValueError):
23
+ """Raised when an unrecognized transport mode is supplied."""
24
+
25
+
26
+ class UnsupportedPresetError(ValueError):
27
+ """Raised when an unrecognized divisor preset is supplied."""
28
+
29
+
30
+ @dataclass
31
+ class WeightResult:
32
+ """Result of a chargeable weight calculation for a single package."""
33
+
34
+ actual_weight_kg: float
35
+ volumetric_weight_kg: float
36
+ chargeable_weight_kg: float
37
+ basis: str # "actual" or "volumetric"
38
+ volume_m3: float
39
+ mode: str
40
+ divisor: Optional[float] = None # None for sea (CBM-based)
41
+
42
+ def to_dict(self) -> dict:
43
+ return {
44
+ "actual_weight_kg": round(self.actual_weight_kg, 6),
45
+ "volumetric_weight_kg": round(self.volumetric_weight_kg, 6),
46
+ "chargeable_weight_kg": round(self.chargeable_weight_kg, 6),
47
+ "basis": self.basis,
48
+ "volume_m3": round(self.volume_m3, 6),
49
+ "mode": self.mode,
50
+ "divisor": self.divisor,
51
+ }
52
+
53
+
54
+ @dataclass
55
+ class ConsolidatedResult:
56
+ """Result of a chargeable weight calculation across multiple packages."""
57
+
58
+ packages: List[WeightResult]
59
+ total_actual_weight_kg: float
60
+ total_volumetric_weight_kg: float
61
+ total_chargeable_weight_kg: float
62
+ basis: str
63
+ mode: str
64
+ per_piece: bool
65
+
66
+ def to_dict(self) -> dict:
67
+ return {
68
+ "packages": [p.to_dict() for p in self.packages],
69
+ "total_actual_weight_kg": round(self.total_actual_weight_kg, 6),
70
+ "total_volumetric_weight_kg": round(self.total_volumetric_weight_kg, 6),
71
+ "total_chargeable_weight_kg": round(self.total_chargeable_weight_kg, 6),
72
+ "basis": self.basis,
73
+ "mode": self.mode,
74
+ "per_piece": self.per_piece,
75
+ }
76
+
77
+
78
+ def _to_cm(value: float, unit: str) -> float:
79
+ unit = unit.lower()
80
+ if unit not in LENGTH_TO_CM:
81
+ raise UnsupportedUnitError(
82
+ f"Unsupported length unit '{unit}'. "
83
+ f"Supported units: {sorted(LENGTH_TO_CM)}"
84
+ )
85
+ return value * LENGTH_TO_CM[unit]
86
+
87
+
88
+ def _to_kg(value: float, unit: str) -> float:
89
+ unit = unit.lower()
90
+ if unit not in WEIGHT_TO_KG:
91
+ raise UnsupportedUnitError(
92
+ f"Unsupported weight unit '{unit}'. "
93
+ f"Supported units: {sorted(WEIGHT_TO_KG)}"
94
+ )
95
+ return value * WEIGHT_TO_KG[unit]
96
+
97
+
98
+ def _resolve_mode(mode) -> Mode:
99
+ if isinstance(mode, Mode):
100
+ return mode
101
+ try:
102
+ return Mode(str(mode).lower())
103
+ except ValueError:
104
+ raise UnsupportedModeError(
105
+ f"Unsupported mode '{mode}'. "
106
+ f"Supported modes: {[m.value for m in Mode]}"
107
+ )
108
+
109
+
110
+ def _resolve_divisor(
111
+ mode: Mode, divisor_preset: Optional[str], divisor: Optional[float]
112
+ ) -> Optional[float]:
113
+ """Resolve the divisor to use, following precedence:
114
+ explicit divisor > named preset > mode default.
115
+ Returns None for SEA mode (CBM-based, no divisor applies).
116
+ """
117
+ if mode == Mode.SEA:
118
+ return None
119
+
120
+ if divisor is not None:
121
+ return divisor
122
+
123
+ if divisor_preset is not None:
124
+ key = divisor_preset.lower()
125
+ if key not in DIVISOR_PRESETS:
126
+ raise UnsupportedPresetError(
127
+ f"Unsupported divisor_preset '{divisor_preset}'. "
128
+ f"Supported presets: {sorted(DIVISOR_PRESETS)}"
129
+ )
130
+ return DIVISOR_PRESETS[key]
131
+
132
+ return DEFAULT_DIVISORS[mode]
133
+
134
+
135
+ def calculate(
136
+ length: float,
137
+ width: float,
138
+ height: float,
139
+ actual_weight: float,
140
+ mode,
141
+ unit: str = "cm",
142
+ weight_unit: str = "kg",
143
+ divisor_preset: Optional[str] = None,
144
+ divisor: Optional[float] = None,
145
+ ) -> WeightResult:
146
+ """
147
+ Calculate volumetric and chargeable weight for a single package.
148
+
149
+ Args:
150
+ length, width, height: Package dimensions.
151
+ actual_weight: Physical (scale) weight of the package.
152
+ mode: Transport mode - "air", "courier", "sea", "road", or "rail"
153
+ (or a Mode enum value).
154
+ unit: Unit for dimensions. One of "cm", "m", "mm", "in", "ft".
155
+ Default "cm".
156
+ weight_unit: Unit for actual_weight. One of "kg", "g", "lb", "oz".
157
+ Default "kg".
158
+ divisor_preset: Optional named divisor preset ("a" = 5000,
159
+ "b" = 6000) to use instead of the mode default. These are
160
+ generic labels for two widely-used divisor values in the
161
+ freight/parcel industry - they are not tied to any specific
162
+ carrier. Ignored for sea mode.
163
+ divisor: Optional explicit divisor (cm^3 per kg) overriding both
164
+ the preset and mode default. Ignored for sea mode.
165
+
166
+ Returns:
167
+ WeightResult with actual, volumetric, and chargeable weights in kg,
168
+ plus the basis ("actual" or "volumetric") used for billing.
169
+
170
+ Notes:
171
+ - For sea freight, volumetric weight is derived from CBM
172
+ (cubic meters) x 1000, following the common "1 CBM = 1000 kg"
173
+ weight-or-measurement (W/M) convention. Some carriers use
174
+ different ratios or apply this only above certain thresholds.
175
+ - Divisors of 5000 and 6000 (presets "a" and "b") are both widely
176
+ used across the freight and parcel industry for different
177
+ modes, services, and regions. Always confirm the applicable
178
+ divisor with your specific carrier or contract for
179
+ billing-critical calculations.
180
+ """
181
+ resolved_mode = _resolve_mode(mode)
182
+
183
+ if length <= 0 or width <= 0 or height <= 0:
184
+ raise ValueError("Dimensions must be positive numbers.")
185
+ if actual_weight < 0:
186
+ raise ValueError("actual_weight must be non-negative.")
187
+
188
+ length_cm = _to_cm(length, unit)
189
+ width_cm = _to_cm(width, unit)
190
+ height_cm = _to_cm(height, unit)
191
+ actual_weight_kg = _to_kg(actual_weight, weight_unit)
192
+
193
+ volume_cm3 = length_cm * width_cm * height_cm
194
+ volume_m3 = volume_cm3 / 1_000_000
195
+
196
+ resolved_divisor = _resolve_divisor(resolved_mode, divisor_preset, divisor)
197
+
198
+ if resolved_mode == Mode.SEA:
199
+ volumetric_weight_kg = volume_m3 * SEA_CBM_TO_KG
200
+ else:
201
+ volumetric_weight_kg = volume_cm3 / resolved_divisor
202
+
203
+ chargeable_weight_kg = max(actual_weight_kg, volumetric_weight_kg)
204
+ basis = "volumetric" if volumetric_weight_kg > actual_weight_kg else "actual"
205
+
206
+ return WeightResult(
207
+ actual_weight_kg=actual_weight_kg,
208
+ volumetric_weight_kg=volumetric_weight_kg,
209
+ chargeable_weight_kg=chargeable_weight_kg,
210
+ basis=basis,
211
+ volume_m3=volume_m3,
212
+ mode=resolved_mode.value,
213
+ divisor=resolved_divisor,
214
+ )
215
+
216
+
217
+ def calculate_consignment(
218
+ packages: List[dict],
219
+ mode,
220
+ unit: str = "cm",
221
+ weight_unit: str = "kg",
222
+ divisor_preset: Optional[str] = None,
223
+ divisor: Optional[float] = None,
224
+ per_piece: bool = False,
225
+ ) -> ConsolidatedResult:
226
+ """
227
+ Calculate chargeable weight across multiple packages in a consignment.
228
+
229
+ Args:
230
+ packages: List of dicts, each with keys "length", "width", "height",
231
+ "actual_weight", and optionally "quantity" (default 1).
232
+ Dimensions/weights are interpreted using the shared `unit`
233
+ and `weight_unit` arguments unless overridden per-package
234
+ with "unit" / "weight_unit" keys.
235
+ mode, unit, weight_unit, divisor_preset, divisor: See `calculate()`.
236
+ per_piece: If True, chargeable weight is computed per package
237
+ (actual vs volumetric compared individually, then summed -
238
+ a convention some couriers use). If False (default), totals
239
+ of actual and volumetric weight are summed separately first,
240
+ and chargeable weight is the max of the two totals.
241
+
242
+ Returns:
243
+ ConsolidatedResult with per-package results and consignment totals.
244
+ """
245
+ if not packages:
246
+ raise ValueError("packages list must not be empty.")
247
+
248
+ results: List[WeightResult] = []
249
+
250
+ for pkg in packages:
251
+ quantity = pkg.get("quantity", 1)
252
+ if quantity <= 0:
253
+ raise ValueError("quantity must be a positive integer.")
254
+
255
+ pkg_unit = pkg.get("unit", unit)
256
+ pkg_weight_unit = pkg.get("weight_unit", weight_unit)
257
+
258
+ result = calculate(
259
+ length=pkg["length"],
260
+ width=pkg["width"],
261
+ height=pkg["height"],
262
+ actual_weight=pkg["actual_weight"],
263
+ mode=mode,
264
+ unit=pkg_unit,
265
+ weight_unit=pkg_weight_unit,
266
+ divisor_preset=divisor_preset,
267
+ divisor=divisor,
268
+ )
269
+
270
+ for _ in range(quantity):
271
+ results.append(result)
272
+
273
+ total_actual = sum(r.actual_weight_kg for r in results)
274
+ total_volumetric = sum(r.volumetric_weight_kg for r in results)
275
+
276
+ if per_piece:
277
+ total_chargeable = sum(r.chargeable_weight_kg for r in results)
278
+ else:
279
+ total_chargeable = max(total_actual, total_volumetric)
280
+
281
+ overall_basis = "volumetric" if total_volumetric > total_actual else "actual"
282
+
283
+ return ConsolidatedResult(
284
+ packages=results,
285
+ total_actual_weight_kg=total_actual,
286
+ total_volumetric_weight_kg=total_volumetric,
287
+ total_chargeable_weight_kg=total_chargeable,
288
+ basis=overall_basis,
289
+ mode=_resolve_mode(mode).value,
290
+ per_piece=per_piece,
291
+ )
@@ -0,0 +1,72 @@
1
+ """
2
+ Volumetric (dimensional) weight divisors and conversion factors.
3
+
4
+ Divisors represent cubic cm per kg.
5
+ Volumetric weight = volume / divisor.
6
+
7
+ Sea freight is handled differently: chargeable weight is typically based
8
+ on CBM (cubic meters), with 1 CBM commonly treated as equivalent to
9
+ 1000 kg (i.e. volume_m3 * 1000 -> kg), under a "weight or measurement"
10
+ (W/M) basis.
11
+
12
+ Common divisor values seen across the freight and parcel industry:
13
+ - 5000: a widely-used divisor for parcel/express-style shipments
14
+ - 6000: a widely-used divisor for general air cargo
15
+ - Road/rail freight commonly uses 3000-5000 depending on region and mode
16
+
17
+ These values vary by carrier, service level, region, and contract terms.
18
+ Always confirm the applicable divisor with your specific carrier for
19
+ billing-critical calculations.
20
+ """
21
+
22
+ from enum import Enum
23
+
24
+
25
+ class Mode(str, Enum):
26
+ AIR = "air"
27
+ COURIER = "courier"
28
+ SEA = "sea"
29
+ ROAD = "road"
30
+ RAIL = "rail"
31
+
32
+
33
+ # Default divisors in cm^3 per kg, applied as: volumetric_weight_kg = volume_cm3 / divisor
34
+ # SEA mode is handled separately via CBM calculation (see calculator.py)
35
+ DEFAULT_DIVISORS = {
36
+ Mode.AIR: 6000,
37
+ Mode.COURIER: 5000,
38
+ Mode.ROAD: 3000,
39
+ Mode.RAIL: 3000,
40
+ # Mode.SEA intentionally omitted - handled via CBM x 1000
41
+ }
42
+
43
+ # Named divisor presets, for users who want to refer to common values by
44
+ # label rather than remembering raw numbers. These are generic industry
45
+ # conventions, not tied to any specific carrier - always confirm the
46
+ # applicable value with your own carrier or contract.
47
+ DIVISOR_PRESETS = {
48
+ "a": 5000,
49
+ "b": 6000,
50
+ }
51
+
52
+ # Sea freight: 1 cubic meter (CBM) is conventionally treated as
53
+ # equivalent to 1000 kg for chargeable weight purposes (the "W/M" -
54
+ # weight or measurement - basis many ocean carriers use as a reference).
55
+ SEA_CBM_TO_KG = 1000
56
+
57
+ # Length unit conversions to centimeters
58
+ LENGTH_TO_CM = {
59
+ "cm": 1.0,
60
+ "m": 100.0,
61
+ "mm": 0.1,
62
+ "in": 2.54,
63
+ "ft": 30.48,
64
+ }
65
+
66
+ # Weight unit conversions to kilograms
67
+ WEIGHT_TO_KG = {
68
+ "kg": 1.0,
69
+ "g": 0.001,
70
+ "lb": 0.45359237,
71
+ "oz": 0.0283495231,
72
+ }
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: logisticspy
3
+ Version: 0.1.0
4
+ Summary: A Python toolkit for logistics and supply chain calculations
5
+ Author: krishnanz550i-cmyk
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/krishnanz550i-cmyk/logisticspy
8
+ Project-URL: Issues, https://github.com/krishnanz550i-cmyk/logisticspy/issues
9
+ Keywords: logistics,supply chain,freight,chargeable weight
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # logisticspy
24
+
25
+ A Python toolkit for logistics and supply chain calculations.
26
+
27
+ `logisticspy` is a growing collection of clean, well-tested tools for
28
+ common logistics and supply chain problems. The first module,
29
+ **`logisticspy.weight`**, calculates volumetric (dimensional) weight and
30
+ chargeable weight for air, courier, sea, road, and rail freight shipments.
31
+ More tools (volume, freight, inventory, and others) will be added over
32
+ time.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install logisticspy
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ You can use the chargeable weight tools either via the top-level package
43
+ or via the `weight` module:
44
+
45
+ ```python
46
+ # Option 1: top-level import
47
+ import logisticspy
48
+
49
+ result = logisticspy.calculate(
50
+ length=60, width=40, height=40, unit="cm",
51
+ actual_weight=18, weight_unit="kg",
52
+ mode="air",
53
+ )
54
+
55
+ # Option 2: import the weight module
56
+ from logisticspy.weight import chargeable_weight
57
+
58
+ result = chargeable_weight.calculate(
59
+ length=60, width=40, height=40, unit="cm",
60
+ actual_weight=18, weight_unit="kg",
61
+ mode="air",
62
+ )
63
+
64
+ print(result.volumetric_weight_kg) # 19.2
65
+ print(result.chargeable_weight_kg) # 19.2
66
+ print(result.basis) # "volumetric"
67
+ ```
68
+
69
+ ## The `weight` module: chargeable weight
70
+
71
+ Carriers bill shipments based on whichever is greater: the **actual
72
+ weight** or the **volumetric weight** (calculated from package
73
+ dimensions). This module implements that calculation cleanly, with
74
+ support for multiple units, transport modes, named divisor presets, and
75
+ multi-package consignments.
76
+
77
+ ### Supported modes and default divisors
78
+
79
+ | Mode | Default divisor (cm³/kg) |
80
+ |-----------|---------------------------|
81
+ | `air` | 6000 |
82
+ | `courier` | 5000 |
83
+ | `road` | 3000 |
84
+ | `rail` | 3000 |
85
+ | `sea` | N/A (uses CBM × 1000, see below) |
86
+
87
+ These are sensible starting defaults based on common conventions seen
88
+ across the freight and parcel industry. They are **not** tied to any
89
+ specific carrier - always confirm the applicable divisor with your own
90
+ carrier or contract for billing-critical calculations.
91
+
92
+ ### Divisor presets
93
+
94
+ Two divisor values - 5000 and 6000 - are both widely used across the
95
+ industry, often for different services, regions, or contracts (sometimes
96
+ even by the same carrier depending on the product). Rather than guessing
97
+ which one applies to your situation, you can refer to them by generic
98
+ preset labels and swap between them easily:
99
+
100
+ | Preset | Divisor |
101
+ |--------|---------|
102
+ | `"a"` | 5000 |
103
+ | `"b"` | 6000 |
104
+
105
+ ```python
106
+ import logisticspy
107
+
108
+ # Same package, two different divisor conventions
109
+ pkg = dict(length=60, width=40, height=40, actual_weight=10, mode="air")
110
+
111
+ result_a = logisticspy.calculate(**pkg, divisor_preset="a") # divisor 5000
112
+ result_b = logisticspy.calculate(**pkg, divisor_preset="b") # divisor 6000
113
+
114
+ print(result_a.volumetric_weight_kg) # 19.2
115
+ print(result_b.volumetric_weight_kg) # 16.0
116
+ ```
117
+
118
+ This makes it easy to compare "what would this shipment cost under each
119
+ convention" without hardcoding either value, and to plug in your own
120
+ carrier's documented divisor (whether that happens to be 5000, 6000, or
121
+ something else entirely) via `divisor_preset` or a raw `divisor=` value.
122
+
123
+ You can also pass an explicit divisor directly, which overrides any preset:
124
+
125
+ ```python
126
+ result = logisticspy.calculate(**pkg, divisor=4500)
127
+ ```
128
+
129
+ ### Units
130
+
131
+ #### Input units
132
+
133
+ Dimensions accept `cm`, `m`, `mm`, `in`, `ft` (default `cm`).
134
+ Weights accept `kg`, `g`, `lb`, `oz` (default `kg`).
135
+
136
+ ```python
137
+ result = logisticspy.calculate(
138
+ length=20, width=15, height=10, unit="in",
139
+ actual_weight=5, weight_unit="lb",
140
+ mode="courier",
141
+ )
142
+ ```
143
+
144
+ #### Output units (always normalized)
145
+
146
+ Regardless of the input units you choose, **all results are returned in
147
+ a single fixed unit system**:
148
+
149
+ | Field | Unit |
150
+ |-------|------|
151
+ | `actual_weight_kg`, `volumetric_weight_kg`, `chargeable_weight_kg` | kilograms (kg) |
152
+ | `volume_m3` | cubic meters (m³) |
153
+
154
+ The input units (`unit`, `weight_unit`) are only used to *interpret* the
155
+ numbers you pass in - they are converted to centimeters and kilograms
156
+ internally before any calculation happens. The output is never expressed
157
+ back in the input units.
158
+
159
+ If you need the result in a different unit (e.g. pounds), convert the
160
+ returned kg value yourself - the library does not provide unit conversion
161
+ on outputs.
162
+
163
+ ### Sea freight (CBM)
164
+
165
+ Sea freight chargeable weight is derived from volume in cubic meters
166
+ (CBM), using the common 1 CBM ≈ 1000 kg convention:
167
+
168
+ ```python
169
+ result = logisticspy.calculate(
170
+ length=1, width=1, height=1, unit="m",
171
+ actual_weight=500, mode="sea",
172
+ )
173
+ print(result.volumetric_weight_kg) # 1000.0
174
+ ```
175
+
176
+ Divisor presets are ignored for sea mode, since it uses a CBM-based
177
+ calculation rather than a divisor.
178
+
179
+ ### Multi-package consignments
180
+
181
+ ```python
182
+ import logisticspy
183
+
184
+ packages = [
185
+ {"length": 50, "width": 40, "height": 40, "actual_weight": 10},
186
+ {"length": 60, "width": 40, "height": 40, "actual_weight": 25, "quantity": 2},
187
+ ]
188
+
189
+ result = logisticspy.calculate_consignment(packages, mode="air")
190
+
191
+ print(result.total_actual_weight_kg)
192
+ print(result.total_volumetric_weight_kg)
193
+ print(result.total_chargeable_weight_kg)
194
+ ```
195
+
196
+ By default, totals are compared (`sum(actual)` vs `sum(volumetric)`).
197
+ Some couriers calculate chargeable weight per package and sum those - use
198
+ `per_piece=True` for that behavior:
199
+
200
+ ```python
201
+ result = logisticspy.calculate_consignment(packages, mode="air", per_piece=True)
202
+ ```
203
+
204
+ ## Roadmap
205
+
206
+ `logisticspy` is designed to grow into a broader logistics toolkit.
207
+ `chargeable_weight` is the first module; additional tools (e.g. volume,
208
+ freight, and inventory calculations) will be added over time.
209
+
210
+ ## Disclaimer
211
+
212
+ This library implements widely-used industry conventions for
213
+ illustrative and estimation purposes. Divisors and CBM ratios vary by
214
+ carrier, service level, region, and contract terms. Always confirm exact
215
+ billing methodology with your carrier or freight forwarder for
216
+ invoicing-critical calculations.
217
+
218
+ ## License
219
+
220
+ MIT
@@ -0,0 +1,9 @@
1
+ logisticspy/__init__.py,sha256=H4X2MNhhLn7vcLUYhvdMIK6_VB6_Cv587R-WOpesDZQ,993
2
+ logisticspy/weight/__init__.py,sha256=94IdASrAiVchplZA6i3TUQoErHgKNA7t4TPVlKX35SE,656
3
+ logisticspy/weight/chargeable_weight.py,sha256=tTjHgBlXAetOEqDQ5bJv8YHeRY35zvzlsUCM-YbPSss,9725
4
+ logisticspy/weight/divisors.py,sha256=I4E5JcZGnxZU8ysfDoudje_rtHtQRVBli0oizGOcwEI,2107
5
+ logisticspy-0.1.0.dist-info/licenses/LICENSE,sha256=dSwPcwW5G_idlsArqi07jZ0aEczyuJ1jpJ92cGvWuAs,1075
6
+ logisticspy-0.1.0.dist-info/METADATA,sha256=bSZ7A-nswv6VzE32-zx8RFyoZA-Vuhz1LqFpN7_-AOE,6784
7
+ logisticspy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ logisticspy-0.1.0.dist-info/top_level.txt,sha256=pXagobTJb9Npe89H4jqfrGhY7dStvu2QKz5F5o7Y5ec,12
9
+ logisticspy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 krishnanz550i-cmyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ logisticspy