Skip to content

Commit

Permalink
Merge 21917c6 into 2618bdd
Browse files Browse the repository at this point in the history
  • Loading branch information
osantana committed Apr 18, 2017
2 parents 2618bdd + 21917c6 commit 6140881
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 171 deletions.
16 changes: 16 additions & 0 deletions correios/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,26 @@ class InvalidPackageDimensionsError(InvalidPackageError):
pass


class InvalidMinPackageDimensionsError(InvalidPackageDimensionsError):
pass


class InvalidMaxPackageDimensionsError(InvalidPackageDimensionsError):
pass


class InvalidPackageWeightError(InvalidPackageError):
pass


class InvalidMinPackageWeightError(InvalidPackageWeightError):
pass


class InvalidMaxPackageWeightError(InvalidPackageWeightError):
pass


class InvalidPackageSequenceError(InvalidPackageError):
pass

Expand Down
190 changes: 123 additions & 67 deletions correios/models/posting.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,12 @@
import math
from datetime import datetime
from decimal import Decimal
from typing import Optional, Sequence, Tuple, Union
from typing import List, Dict # noqa: F401
from typing import Optional, Sequence, Tuple, Union, List, Dict # noqa: F401

from PIL import Image

from correios import DATADIR
from correios.exceptions import (InvalidAddressesError, InvalidEventStatusError,
InvalidPackageSequenceError, InvalidTrackingCodeError,
PostingListError, InvalidPackageDimensionsError,
InvalidPackageWeightError)
from correios import exceptions
from .address import Address, ZipCode
from .data import SERVICE_PAC, TRACKING_EVENT_TYPES, TRACKING_STATUS
from .user import Contract # noqa: F401
Expand Down Expand Up @@ -66,12 +62,12 @@ def __init__(self,

def _get_event_status_data(self, event_type, status_code):
if event_type not in TRACKING_EVENT_TYPES:
raise InvalidEventStatusError("{} is not valid".format(event_type))
raise exceptions.InvalidEventStatusError("{} is not valid".format(event_type))

try:
return TRACKING_STATUS[event_type, status_code]
except KeyError:
raise InvalidEventStatusError("{} is not valid".format(event_type))
raise exceptions.InvalidEventStatusError("{} is not valid".format(event_type))

@property
def display_event_type(self):
Expand Down Expand Up @@ -156,16 +152,16 @@ def __init__(self, code: str) -> None:

def _validate(self):
if len(self.prefix) != TRACKING_CODE_PREFIX_SIZE or not self.prefix.isalpha():
raise InvalidTrackingCodeError("Invalid tracking code prefix {}".format(self.prefix))
raise exceptions.InvalidTrackingCodeError("Invalid tracking code prefix {}".format(self.prefix))

if len(self.suffix) != TRACKING_CODE_SUFFIX_SIZE or not self.suffix.isalpha():
raise InvalidTrackingCodeError("Invalid tracking code suffix {}".format(self.suffix))
raise exceptions.InvalidTrackingCodeError("Invalid tracking code suffix {}".format(self.suffix))

if len(self.number) != TRACKING_CODE_NUMBER_SIZE or not self.number.isnumeric():
raise InvalidTrackingCodeError("Invalid tracking code number {}".format(self.number))
raise exceptions.InvalidTrackingCodeError("Invalid tracking code number {}".format(self.number))

if self._digit is not None and self._digit != self.calculate_digit(self.number):
raise InvalidTrackingCodeError("Invalid tracking code number {} or digit {} (must be {})".format(
raise exceptions.InvalidTrackingCodeError("Invalid tracking code number {} or digit {} (must be {})".format(
self.number,
self._digit,
self.calculate_digit(self.number))
Expand All @@ -182,15 +178,15 @@ def calculate_digit(cls, number: str) -> int:
numbers = [int(c) for c in number if c.isdigit()]

multipliers = [8, 6, 4, 2, 3, 5, 9, 7]
module = sum(multipliers[i] * digit for i, digit in enumerate(numbers)) % 11
mod = sum(multipliers[i] * digit for i, digit in enumerate(numbers)) % 11

if not module:
if not mod:
return 5

if module == 1:
if mod == 1:
return 0

return 11 - module
return 11 - mod

@classmethod
def create_range(cls, start: Union[str, 'TrackingCode'], end: Union[str, 'TrackingCode']):
Expand All @@ -201,19 +197,22 @@ def create_range(cls, start: Union[str, 'TrackingCode'], end: Union[str, 'Tracki
end = TrackingCode(end)

if start.prefix != end.prefix:
raise InvalidTrackingCodeError("Different tracking code prefixes: {} != {}".format(start.prefix,
end.prefix))
raise exceptions.InvalidTrackingCodeError(
"Different tracking code prefixes: {} != {}".format(start.prefix, end.prefix)
)

if start.suffix != end.suffix:
raise InvalidTrackingCodeError("Different tracking code suffixes: {} != {}".format(start.suffix,
end.suffix))
raise exceptions.InvalidTrackingCodeError(
"Different tracking code suffixes: {} != {}".format(start.suffix, end.suffix)
)

start_number = int(start.number)
end_number = int(end.number)

if start_number > end_number:
raise InvalidTrackingCodeError("Invalid range numbers: {} > {}".format(start_number,
end_number))
raise exceptions.InvalidTrackingCodeError(
"Invalid range numbers: {} > {}".format(start_number, end_number)
)

code_range = range(int(start.number), int(end.number) + 1)
return [TrackingCode(start.prefix + "{:08}".format(n) + start.suffix) for n in code_range]
Expand Down Expand Up @@ -272,34 +271,86 @@ def __init__(self,
Package.validate(package_type, width, height, length, diameter, service, weight)

if len(sequence) != 2 or sequence[0] > sequence[1]:
raise InvalidPackageSequenceError("Package must be a tuple with 2 elements: (number, total)")
raise exceptions.InvalidPackageSequenceError("Package must be a tuple with 2 elements: (number, total)")

self.package_type = package_type
self.width = math.ceil(width) # cm
self.height = math.ceil(height) # cm
self.length = math.ceil(length) # cm
self.diameter = math.ceil(diameter) # in cm
self.weight = math.ceil(weight) # in grams

self.real_width = width # cm
self.real_height = height # cm
self.real_length = length # cm
self.real_diameter = diameter # in cm
self.real_weight = diameter # in grams
self.sequence = sequence
self.service = service

@property
def volumetric_weight(self):
def width(self):
return max(MIN_WIDTH, int(math.ceil(self.real_width)))

@width.setter
def width(self, width):
Package._validate_dimension("width", width, MAX_WIDTH)
self.real_width = width

@property
def height(self):
return max(MIN_HEIGHT, int(math.ceil(self.real_height)))

@height.setter
def height(self, height):
Package._validate_dimension("height", height, MAX_HEIGHT)
self.real_height = height

@property
def length(self):
return max(MIN_LENGTH, int(math.ceil(self.real_length)))

@length.setter
def length(self, length):
Package._validate_dimension("length", length, MAX_LENGTH)
self.real_length = length

@property
def diameter(self):
return max(MIN_DIAMETER, int(math.ceil(self.real_diameter)))

@diameter.setter
def diameter(self, diameter):
Package._validate_dimension("diameter", diameter, MAX_DIAMETER)
self.real_diameter = diameter

@property
def weight(self):
return int(math.ceil(self.real_weight))

@weight.setter
def weight(self, weight):
if weight <= 0:
raise exceptions.InvalidMinPackageWeightError("Invalid weight {!r}g".format(weight))

if self.service and weight > self.service.max_weight:
raise exceptions.InvalidMaxPackageWeightError(
"Max weight exceeded {!r}g (max. {!r}g)".format(weight, self.service.max_weight)
)
self.real_weight = weight

@property
def volumetric_weight(self) -> int:
return Package.calculate_volumetric_weight(self.width, self.height, self.length)

@property
def posting_weight(self):
def posting_weight(self) -> int:
return Package.calculate_posting_weight(self.weight, self.volumetric_weight)

@classmethod
def calculate_volumetric_weight(cls, width, height, length):
return math.ceil((width * height * length) / IATA_COEFICIENT)
def calculate_volumetric_weight(cls, width, height, length) -> int:
return int(math.ceil((width * height * length) / IATA_COEFICIENT))

@classmethod
def calculate_posting_weight(cls, weight, volumetric_weight):
def calculate_posting_weight(cls, weight, volumetric_weight) -> int:
if volumetric_weight <= VOLUMETRIC_WEIGHT_THRESHOLD:
return weight
return math.ceil(max(volumetric_weight, weight))
return int(math.ceil(max(volumetric_weight, weight)))

@classmethod
def calculate_insurance(cls,
Expand All @@ -322,50 +373,54 @@ def validate(cls,
service: Optional[Service] = None,
weight: Union[float, int] = 0) -> None:

width = math.ceil(width)
height = math.ceil(height)
length = math.ceil(length)
diameter = math.ceil(diameter)
weight = math.ceil(weight)
width = int(math.ceil(width))
height = int(math.ceil(height))
length = int(math.ceil(length))
diameter = int(math.ceil(diameter))
weight = int(math.ceil(weight))

if service and service.max_weight and weight > service.max_weight:
raise InvalidPackageWeightError("Max weight exceeded {!r}g (max. {!r}g)".format(weight, service.max_weight))
if weight <= 0 or (service and service.max_weight and weight > service.max_weight):
raise exceptions.InvalidMaxPackageWeightError(
"Max weight exceeded {!r}g (max. {!r}g)".format(weight, service.max_weight)
)

if package_type == Package.TYPE_ENVELOPE:
if any([width, height, length, diameter]):
raise InvalidPackageDimensionsError("Invalid dimensions: {}x{}x{}".format(width, height, length))
raise exceptions.InvalidPackageDimensionsError(
"Invalid dimensions: {}x{}x{}".format(width, height, length)
)
return

if package_type == Package.TYPE_BOX:
if diameter:
raise InvalidPackageDimensionsError("Package does not use diameter: {}".format(diameter))

if not MIN_WIDTH <= width <= MAX_WIDTH:
raise InvalidPackageDimensionsError("Invalid width (range 11~105): {}".format(width))

if not MIN_HEIGHT <= height <= MAX_HEIGHT:
raise InvalidPackageDimensionsError("Invalid height (range 2~105): {}".format(height))

if not MIN_LENGTH <= length <= MAX_LENGTH:
raise InvalidPackageDimensionsError("Invalid length (range 16~105): {}".format(length))

if not MIN_SIZE <= (width + height + length) <= MAX_SIZE:
raise InvalidPackageDimensionsError("Invalid dimensions: {}x{}x{}".format(width, height, length))

raise exceptions.InvalidPackageDimensionsError(
"Package does not use diameter: {}".format(diameter)
)

cls._validate_dimension("width", width, MAX_WIDTH)
cls._validate_dimension("height", height, MAX_HEIGHT)
cls._validate_dimension("length", length, MAX_LENGTH)
cls._validate_dimension("sum of dimensions", width + height + length, MAX_SIZE)
return

# Volume.TYPE_CYLINDER
if width or height:
raise InvalidPackageDimensionsError("Cylinder does not use width/height: {}x{}".format(width, height))
raise exceptions.InvalidPackageDimensionsError(
"Cylinder does not use width/height: {}x{}".format(width, height)
)

if not MIN_CYLINDER_LENGTH <= length <= MAX_CYLINDER_LENGTH:
raise InvalidPackageDimensionsError("Invalid length (range 18~105): {}".format(length))
cls._validate_dimension("length", length, MAX_CYLINDER_LENGTH)
cls._validate_dimension("diameter", diameter, MAX_DIAMETER)
cls._validate_dimension("cylinder size", length + 2 * diameter, MAX_CYLINDER_SIZE)

if not MIN_DIAMETER <= diameter <= MAX_DIAMETER:
raise InvalidPackageDimensionsError("Invalid diameter (range 5~91): {}".format(diameter))
@classmethod
def _validate_dimension(cls, name, value, maximum):
msg = "Invalid {} (range 0~{}): {}".format(name, maximum, value)
if value <= 0:
raise exceptions.InvalidMinPackageDimensionsError(msg)

if (length + 2 * diameter) > MAX_CYLINDER_SIZE:
raise InvalidPackageDimensionsError("Invalid dimensions: {}x{}".format(length, diameter))
if value > maximum:
raise exceptions.InvalidMaxPackageDimensionsError(msg)


class ShippingLabel:
Expand Down Expand Up @@ -408,7 +463,7 @@ def __init__(self,
longitude: Optional[float] = 0.0) -> None:

if sender == receiver:
raise InvalidAddressesError("Sender and receiver cannot be the same")
raise exceptions.InvalidAddressesError("Sender and receiver cannot be the same")

if logo is None:
logo = os.path.join(DATADIR, "default_logo.png")
Expand Down Expand Up @@ -559,11 +614,12 @@ def add_shipping_label(self, shipping_label: ShippingLabel):
self.sender = shipping_label.sender

if shipping_label.tracking_code.short in self.shipping_labels:
raise PostingListError("Shipping label {!r} already in posting list".format(shipping_label))
raise exceptions.PostingListError("Shipping label {!r} already in posting list".format(shipping_label))

if shipping_label.posting_card != self.posting_card:
raise PostingListError("Invalid posting card: {} != {}".format(shipping_label.posting_card,
self.posting_card))
raise exceptions.PostingListError(
"Invalid posting card: {} != {}".format(shipping_label.posting_card, self.posting_card)
)
self.shipping_labels[shipping_label.tracking_code.short] = shipping_label
shipping_label.posting_list = self

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class Meta:
height = LazyFunction(lambda: random.randint(2, 30))
length = LazyFunction(lambda: random.randint(16, 30))
weight = LazyFunction(lambda: random.randint(1, 100) * 100)
service = LazyFunction(lambda: random.choice(_services))
sequence = Sequence(lambda n: (n, n + 1))


Expand Down
Loading

0 comments on commit 6140881

Please sign in to comment.