חזור לתיקייה: גליון 5

matamazon

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה

    matamazon

    02340124

    מבוא לתכנות מערכות

    רשימות הרצאה לקריאה בשבת

    matamazon

    הופק מתוך תקיית התואר של שקד

    uni-folder.vercel.app

    matamazon.py

    הערה (קובץ קוד)

    קובץ זה הוא קובץ קוד מסוג PYTHON.

    import io, json, sys
    
    CUSTOMER = "Customer"
    SUPPLIER = "Supplier"
    PRODUCT = "Product"
    ORDER = "Order"
    EXCEPTED_FLAGS = ["-l", "-s", "-o", "-os"]
    
    class FlagException(Exception):
        pass
    
    class InvalidCommandException(Exception):
        pass
    
    class InvalidNumberOfArgumentsException(Exception):
        pass
    
    class InvalidIdException(Exception):
        pass
    
    class InvalidPriceException(Exception):
        pass
    
    class Entity:
        """
        A parent class of Consumers and Suppliers.
        """
    
        def __init__(self, id: int, name: str, city: str, address: str):
            if id < 0:
                raise InvalidIdException(f'Negative ID: {id}')
            self.id = id
            self.name = name
            self.city = city
            self.address = address
    
    
        def __str__(self):
            return f"(id={self.id}, name='{self.name}', city='{self.city}', address='{self.address}')"
    
    class Customer(Entity):
        """
        Represents a customer in the Matamazon system.
    
        Required fields (per specification):
            - id (int): Unique non-negative integer identifier.
            - name (str): Customer name.
            - city (str): Customer city.
            - address (str): Customer shipping address.
    
        Exceptions:
            InvalidIdException: If 'id' is not valid according to the specification
        """
    
        def __str__(self):
            return CUSTOMER + super().__str__()
    
    class Supplier(Entity):
        """
            Represents a supplier in the Matamazon system.
    
            Required fields (per specification):
                - id (int): Unique non-negative integer identifier.
                - name (str): Supplier name.
                - city (str): Warehouse city (origin city for shipping).
                - address (str): Warehouse address.
    
            Exceptions:
                InvalidIdException: If 'id' is not valid according to the specification.
            """
    
        def __str__(self):
            return SUPPLIER + super().__str__()
    
    class Product:
        """
            Represents a product sold on the Matamazon website.
    
            Required fields (per specification):
                - id (int): Unique non-negative integer identifier.
                - name (str): Product name.
                - price (float): Non-negative price.
                - supplier_id (int): ID of the supplier that provides the product.
                - quantity (int): Non-negative quantity in stock.
    
            Exceptions:
                InvalidIdException:
                    - If id/supplier_id/quantity is invalid per specification.
                InvalidPriceException:
                    - If price is invalid (e.g., negative).
            """
    
        def __init__(self, id: int, name: str, price: float, supplier_id: int, quantity: int):
            if id < 0:
                raise InvalidIdException(f'Negative ID: {id}')
            if supplier_id < 0:
                raise InvalidIdException(f'Negative Supplier ID: {supplier_id}')
            if price < 0:
                raise InvalidPriceException(f'Negative Price: {price}')
            if quantity < 0:
                raise Exception(f'Negative Quantity: {quantity}')
            self.id = id
            self.name = name
            self.price = price
            self.supplier_id = supplier_id
            self.quantity = quantity
    
    
        def __str__(self):
            return (f"Product(id={self.id}, name='{self.name}', price={self.price}, supplier_id={self.supplier_id}, "
                    f"quantity={self.quantity})")
    
        def __lt__(self, other):
            """
            Compare this product with another product based on price.
            """
    
            return self.price < other.price
    
        def contains(self, string: str) -> bool:
            """
            Check if a specified substring is part of the product's name.
    
            Args:
                string (str): The substring to search for.
    
            Returns:
                bool: True if the substring is in the name, False otherwise.
            """
    
            return string in self.name
    
        def update(self, other: 'Product') -> None:
            """
            Update the current product's details using another product's data.
    
            Args:
                other (Product): The product object containing the updated name, price, and quantity.
            """
    
            self.name = other.name
            self.price = other.price
            self.quantity = other.quantity
    
        def add_amount_to_quantity(self, amount: int) -> None:
            """
            Add a specified amount to the product's current stock quantity.
            Can also decrease the quantity if the provided amount is negative.
    
            Args:
                amount (int): The amount to add to the inventory.
            """
    
            self.quantity += amount
    
        def calculate_price(self, quantity: int) -> float:
            """
            Calculate the total price for a given quantity of this product.
    
            Args:
                quantity (int): The amount of items to calculate the price for.
    
            Returns:
                float: The total calculated price.
            """
    
            return quantity * self.price
    
        def below_max_price(self, max_price: int) -> bool:
            """
            Check if the product's price is less than or equal to a given maximum price.
    
            Args:
                max_price (int): The maximum price threshold, or None if no limit is applied.
    
            Returns:
                bool: True if the price is within the limit or if max_price is None, False otherwise.
            """
    
            return (max_price is None) or (self.price <= max_price)
    
    class Order:
        """
            Represents a placed order.
    
            Required fields (per specification):
                - id (int): Unique non-negative integer identifier (assigned by the system).
                - customer_id (int): ID of the customer who placed the order.
                - product_id (int): ID of the ordered product.
                - quantity (int): Ordered quantity (non-negative integer).
                - total_price (float): Total price for the order (non-negative).
    
            Exceptions:
                InvalidIdException:
                    - If one of the ID fields is invalid.
                InvalidPriceException:
                    - If total_price is invalid.
            """
    
        def __init__(self, id: int, customer_id: int, product_id: int, quantity: int, total_price: float):
            if id < 0:
                raise InvalidIdException(f'Negative ID: {id}')
            if customer_id < 0:
                raise InvalidIdException(f'Negative Customer ID: {customer_id}')
            if product_id < 0:
                raise InvalidIdException(f'Negative Product ID: {product_id}')
            if total_price < 0:
                raise InvalidPriceException(f'Negative Price Total: {total_price}')
            if quantity < 0:
                raise Exception(f'Negative Quantity: {quantity}')
            self.id = id
            self.customer_id = customer_id
            self.product_id = product_id
            self.quantity = quantity
            self.total_price = total_price
    
    
        def __str__(self):
            return (f"Order(id={self.id}, customer_id={self.customer_id}, product_id={self.product_id},"
                    f" quantity={self.quantity}, total_price={self.total_price})")
    
    class MatamazonSystem:
        """
            Main system class that stores and manages customers, suppliers, products and orders.
    
            The system must support:
                - Registering customers/suppliers (with unique IDs across both types).
                - Adding/updating products (must validate supplier existence).
                - Placing orders (validate product existence and stock).
                - Removing objects by ID and type (with dependency constraints).
                - Searching products by name/query and optional max price.
                - Exporting system state to a text file (customers/suppliers/products only).
                - Exporting orders to JSON grouped by supplier origin city.
    
            Notes:
                - The specification does not require specific internal fields. Any data structures are allowed,
                  as long as the behaviors match the spec.
                - A parameterless constructor is required.
            """
    
        def __init__(self):
            """
            Initialize an empty Matamazon system.
    
            Requirements:
                - Must be parameterless.
                - Internal collections may be chosen freely (dict/list, etc.).
            """
    
            self.customers = {}
            self.suppliers = {}
            self.products = {}
            self.orders = {}
            self.id_counter = 1
    
        def is_in_ids(self, id: int, class_type: str) -> bool:
            """
            Check if an ID exists in the system for a specific entity type.
    
            Args:
                id (int): The identifier to search for.
                class_type (str): The type of the entity ("Customer", "Supplier", "Product", or "Order").
    
            Returns:
                bool: True if the ID exists in the corresponding dictionary, False otherwise.
    
            Raises:
                Exception: If an unsupported class_type is provided.
            """
    
            class_type = class_type.lower().strip()
            if class_type == CUSTOMER.lower():
                return id in self.customers
            elif class_type == SUPPLIER.lower():
                return id in self.suppliers
            elif class_type == PRODUCT.lower():
                return id in self.products
            elif class_type == ORDER.lower():
                return id in self.orders
            else:
                raise Exception(f'Invalid Type: {class_type}')
    
        def check_dependencies(self, id: int, class_type: str) -> None:
            """
            Ensure that an entity is not linked to any active orders in the system.
            Used to validate deletion requests.
    
            Args:
                id (int): The ID of the entity to check.
                class_type (str): The type of the entity ("Customer", "Supplier", or "Product").
    
            Raises:
                InvalidIdException: If the entity is still part of an existing order.
            """
    
            class_type = class_type.lower().strip()
            for order in self.orders.values():
                if (class_type == CUSTOMER.lower() and order.customer_id == id) or \
                        (class_type == SUPPLIER.lower() and self.products[order.product_id].supplier_id == id) or \
                        (class_type == PRODUCT.lower() and order.product_id == id):
                    raise InvalidIdException(f"The order: {order.id} depends on the {class_type}: {id}")
    
        def get_origin_city(self, order: Order) -> str:
            """
            Retrieve the origin city of an order based on its supplier's warehouse location.
    
            Args:
                order (Order): The order object.
    
            Returns:
                str: The city of the supplier who provides the product in the order.
            """
    
            return self.suppliers[self.products[order.product_id].supplier_id].city
    
        def register_entity(self, entity: Entity, is_customer: bool) -> None:
            """
            Register a Customer or Supplier in the system.
    
            Args:
                entity: A Customer or Supplier object.
                is_customer (bool): True if entity is Customer, False if entity is Supplier.
    
            Raises:
                InvalidIdException:
                    - If the entity ID is invalid.
                    - If the entity ID already exists in the system (note: IDs must be unique across
                      customers AND suppliers).
            """
    
            # if self.isInIds(entity.id, CUSTOMER): TODO: make sure
            #     raise InvalidIdException(f'This ID: {entity.id} already exists in Customers')
            # if self.isInIds(entity.id, SUPPLIER):
            #     raise InvalidIdException(f'This ID: {entity.id} already exists in Suppliers')
            type = CUSTOMER if is_customer else SUPPLIER
            if self.is_in_ids(entity.id, type):
                raise InvalidIdException(f'This ID: {entity.id} already exists in {type.lower()}s')
            if is_customer:
                self.customers[entity.id] = entity
            else:
                self.suppliers[entity.id] = entity
    
        def add_or_update_product(self, product: Product) -> None:
            """ Add a new product or update an existing product.
    
            Behavior:
                - If product does not exist in system: add it.
                - If product exists:
                    - It must belong to the same supplier as the existing one (same supplier_id),
                      otherwise raise InvalidIdException.
                    - Update the stored product's fields according to the new product.
    
            Args:
                product: A Product object.
    
            Raises:
                InvalidIdException:
                    - If the supplier_id does not exist in the system.
                    - If attempting to update a product but supplier_id differs from the existing product."""
    
            temp = self.products.get(product.id)
            if temp is None:
                if not self.is_in_ids(product.supplier_id, SUPPLIER):
                    raise InvalidIdException(f"The supplier {product.supplier_id} does not exist in the system.")
                self.products[product.id] = product
            else:
                if temp.supplier_id == product.supplier_id:
                    temp.update(product)
                else:
                    raise InvalidIdException(f"This product already exists with a different supplier (The ID is: {temp.supplier_id})")
    
        def place_order(self, customer_id: int, product_id: int, quantity: int = 1) -> str:
            """
            Place an order for a product by a customer.
    
            Args:
                customer_id (int): Customer ID.
                product_id (int): Product ID.
                quantity (int, optional): Quantity to order. Defaults to 1.
    
            Returns:
                str: Status message according to specification:
                    - "The order has been accepted in the system"
                    - "The product does not exist in the system"
                    - "The quantity requested for this product is greater than the quantity in stock"
    
            Behavior:
                - If product does not exist: return the relevant message.
                - If quantity requested > stock: return the relevant message.
                - Otherwise:
                    - Decrease product stock by quantity.
                    - Create a new Order with an auto-incremented system ID (starting at 1).
                    - Store the order in the system.
                    - Return success message.
    
            Notes:
                - The specification assumes quantity is an integer.
            """
    
            if not self.is_in_ids(product_id, PRODUCT):
                return "The product does not exist in the system"
            if not self.is_in_ids(customer_id, CUSTOMER):
                raise InvalidIdException(f"The customer {customer_id} does not exist in the system.")
            product = self.products[product_id]
            if product.quantity < quantity:
                return "The quantity requested for this product is greater than the quantity in stock"
            product.add_amount_to_quantity(-quantity)
            price = product.calculate_price(quantity)
            self.orders[self.id_counter] = Order(self.id_counter, customer_id, product_id, quantity, price)
            self.id_counter += 1
            return "The order has been accepted in the system"
    
        def remove_object(self, _id: int, class_type: str) -> int | None:
            """
            Remove an object from the system by ID and type.
    
            Args:
                _id (int): Object ID to remove.
                class_type (str): One of: "Customer", "Supplier", "Product", "Order"
                                  (exact casing/spelling per assignment).
    
            Returns:
                int | None:
                    - If removing an Order: return the ordered quantity of that order (to restore stock).
                    - Otherwise: no return value required.
    
            Raises:
                InvalidIdException:
                    - If _id is not a valid non-negative integer.
                    - If attempting to remove a Customer/Supplier/Product that still has dependent orders
                      in the system (i.e., orders that were not removed).
                    - Additional InvalidIdException conditions as required by specification.
            """
    
            if _id < 0:
                raise InvalidIdException(f"Negative ID: {_id}")
            class_type = class_type.lower().strip()
            if not self.is_in_ids(_id, class_type):
                raise InvalidIdException(f"There is no {class_type} with ID: {_id}")
            if class_type == ORDER.lower():
                order = self.orders.pop(_id)
                self.products[order.product_id].add_amount_to_quantity(order.quantity)
                return order.quantity
            self.check_dependencies(_id, class_type)
            if class_type == CUSTOMER.lower():
                self.customers.pop(_id)
            if class_type == SUPPLIER.lower():
                self.suppliers.pop(_id)
            if class_type == PRODUCT.lower():
                self.products.pop(_id)
            return None
    
        def search_products(self, query, max_price=None):
            """
            Search products by query in the product name, and optionally filter by max_price.
    
            Args:
                query (str): Product name or part of product name.
                max_price (float, optional): If provided, only return products with price <= max_price.
    
            Returns:
                list[Product]:
                    - Products that match the query and have quantity != 0,
                    - Sorted by ascending price.
                    - If no matching products exist, return an empty list.
            """
    
            list_of_products = []
            for product in self.products.values():
                if product.quantity > 0 and product.below_max_price(max_price) and product.contains(query):
                    list_of_products.append(product)
            return sorted(list_of_products)
    
        def export_system_to_file(self, path: str):
            """
            Export system state (customers, suppliers, products) to a text file.
    
            Args:
                path (str): Output file path.
    
            Behavior:
                - Write each object on its own line, using the object's print/str representation.
                - Orders must NOT be included.
                - No constraint on the ordering of objects in the output.
    
            Raises:
                OSError (or any file-open exception): Must be propagated to the caller.
            """
    
            connected: str = ""
    
            for customer in self.customers.values():
                connected += f"{customer}\n"
            for supplier in self.suppliers.values():
                connected += f"{supplier}\n"
            for product in self.products.values():
                connected += f"{product}\n"
            
            with open(path, "w") as file:
                file.write(connected)
    
        def export_orders_dict(self) -> dict:
            """
            Export orders in JSON format grouped by origin city.
    
            Behavior (per specification):
                - Produce a JSON object where:
                    - Keys: origin city (supplier city) for each order.
                    - Values: list of strings representing orders (format as specified in section 4.1.4).
                - Order lists can be in any order.
                - No requirement on key ordering.
    
            Notes:
                - The order origin city is the supplier city of the ordered product.
            """
    
            # product -> origin city
            product_to_supplier = {product.id: product.supplier_id for product in self.products.values()}
            supplier_to_city = {supplier.id: supplier.city for supplier in self.suppliers.values()}
            product_to_supplier = {key: supplier_to_city[value] for key, value in product_to_supplier.items()}
    
            # building the json
            json_output = {val: [] for val in product_to_supplier.values()}
            for order in self.orders.values():
                json_output[product_to_supplier[order.product_id]].append(order.__str__())
            
            # remove empty cities
            for key in list(json_output.keys()):
                if not json_output[key]:
                    del json_output[key]
    
            return json_output
    
        def export_orders(self, out_file: io.IOBase):
            """
            Export orders in JSON format grouped by origin city.
    
            Args:
                out_file (file-like)
    
            Behavior (per specification):
                - Produce a JSON object where:
                    - Keys: origin city (supplier city) for each order.
                    - Values: list of strings representing orders (format as specified in section 4.1.4).
                - Order lists can be in any order.
                - No requirement on key ordering.
    
            Raises:
                Any exception during writing: Must be propagated to the caller.
    
            Notes:
                - The order origin city is the supplier city of the ordered product.
            """
    
            json.dump(self.export_orders_dict(), out_file)
    
    def load_system_from_file(path: str) -> MatamazonSystem:
        """
        Load a MatamazonSystem from an input file.
    
        Args:
            path (str): Path to a text file containing customers, suppliers and products.
    
        Returns:
            MatamazonSystem: Initialized system with the data found in the file.
    
        Behavior:
            - The file lines contain objects in the format produced by export_system_to_file (section 4.2).
            - Lines may appear in any order (e.g., product lines can appear before supplier lines).
            - Illegal lines may be ignored.
            - If an exception occurs during the creation of any required object due to invalid data,
              the function should stop and propagate the exception (as specified).
    
        Notes:
            - The assignment hints that eval() may be used.
        """
    
        file_contents = ""
        with open(path, "r") as file:
            file_contents = file.read()
        file_list: list[str] = file_contents.split("\n")
    
        system = MatamazonSystem()
        orders: list[Order] = []
        products: list[Product] = []
        for item in file_list:
            try:
                obj = eval(item)
            except (SyntaxError, NameError, TypeError, ZeroDivisionError, ValueError):
                continue # ignoring illegal lines but not ignoring constructor fails
    
            if item.startswith(ORDER):
                orders.append(obj)
            elif item.startswith(PRODUCT):
                products.append(obj)
            else: # it's written in the instructions that we don't need to check correctness of lines.
                system.register_entity(obj, item.startswith(CUSTOMER))
        
        # products are dependent one customers and suppliers
        for product in products:
            system.add_or_update_product(product)
        
        # orders are dependent on products and everything else
        for order in orders: 
            system.place_order(order.customer_id, order.product_id, order.quantity)
        
        return system
    
    def check_args_number(num_of_arguments: int, command: str) -> None:
        """
        Validate the number of arguments for a given command.
    
        Raises:
            InvalidCommandException: If the command is not recognized.
            InvalidNumberOfArgumentsException: If the argument count does not match the command's requirements.
        """
    
        correct: bool = False
    
        if (command == "register") or (command == "add") or (command == "update"):
            correct = (num_of_arguments == 5)
        elif (command == "order"):
            correct = (num_of_arguments == 2) or (num_of_arguments == 3)
        elif (command == "remove"):
            correct = (num_of_arguments == 2)
        elif (command == "search"):
            correct = (num_of_arguments == 1) or (num_of_arguments == 2)
        else:
            raise InvalidCommandException()
    
        if not correct:
            raise InvalidNumberOfArgumentsException()
    def parse_register(args: list[str], system: MatamazonSystem) -> None:
        """
        Parse arguments and register a new Customer or Supplier in the system.
        """
    
        if args[0] == "customer":
            system.register_entity(Customer(int(args[1]), args[2], args[3], args[4]), True)
        else:
            system.register_entity(Supplier(int(args[1]), args[2], args[3], args[4]), False)
    
    def parse_add_or_update(args: list[str], system: MatamazonSystem) -> None:
        """
        Parse arguments to create a Product object, then add or update it in the system.
        """
    
        system.add_or_update_product(Product(int(args[0]), args[1], float(args[2]), int(args[3]), int(args[4])))
    
    def parse_order(args: list[str], system: MatamazonSystem) -> None:
        """
        Parse arguments and place an order in the system, handling optional quantity.
        """
    
        if (len(args) == 3):
            system.place_order(int(args[0]), int(args[1]), int(args[2]))
        else:
            system.place_order(int(args[0]), int(args[1]))
    
    def parse_remove(args: list[str], system: MatamazonSystem) -> None:
        """
        Parse arguments and remove the specified object (by ID and type) from the system.
        """
    
        system.remove_object(int(args[1]), args[0])
    
    def list_of_products_to_strings(products: list[Product]) -> list[str]:
        """
        Convert a list of Product objects to a list of their string representations.
        """
    
        result = []
    
        for product in products:
            result.append(product.__str__())
    
        return result
    
    def parse_search(args: list[str], system: MatamazonSystem) -> None:
        """
        Parse arguments, perform a product search in the system, and print the results.
        """
    
        if (len(args) == 2):
            print(list_of_products_to_strings(system.search_products(args[0], float(args[1]))))
        else:
            print(list_of_products_to_strings(system.search_products(args[0])))
    
    def unpack_underscores(words: list[str]):
        """
        Replace all underscores with spaces in a given list of strings in-place.
        """
    
        for i in range(0, len(words)):
            words[i] = words[i].replace("_", " ")
    
    def parse_command(command: str, system: MatamazonSystem) -> None:
        """
        Parse a single command string and route it to the appropriate handler function.
        """
    
        args = command.split(" ")
        command_name = args[0]
        args.remove(command_name)
        check_args_number(len(args), command_name)
        unpack_underscores(args)
    
        if (command_name == "register"):
            parse_register(args, system)
        elif (command_name == "add") or (command_name == "update"):
            parse_add_or_update(args, system)
        elif (command_name == "order"):
            parse_order(args, system)
        elif (command_name == "remove"):
            parse_remove(args, system)
        else:  # command existence checked at check_args_number
            parse_search(args, system)
    
    def parse_log_file(path: str, system: MatamazonSystem) -> None:
        """
        Read a log file line by line and parse each valid command to update the system.
        """
    
        commands: list[str] = []
        with open(path, "r") as file:
            commands = [command for command in file.read().split("\n") if command.strip() != ""]
    
        for command in commands:
            parse_command(command, system)
    
    def create_system(is_system_file: bool, system_path: str) -> MatamazonSystem:
        """
        Initialize and return a MatamazonSystem, either by creating a new empty system
        or loading an existing one from a file.
        """
    
        if not is_system_file:
            return MatamazonSystem()
    
        return load_system_from_file(system_path)
    
    def export(system: MatamazonSystem, print_to_file: bool, path: str, print_system: bool, system_path: str) -> None:
        """
        Export system orders (to standard output or a JSON file) and optionally
        export the entire system state to a text file.
        """
    
        if not print_to_file:
            print(json.dumps(system.export_orders_dict()))
        else:
            file = open(path, "w")
            system.export_orders(file)
            file.close()
    
        if print_system:
            system.export_system_to_file(system_path)
    
    def flags_to_dict(command_line_flags: list[str]) -> dict:
        """
        Parse command-line flag pairs into a dictionary.
        Validates flags and ensures the mandatory log file flag is provided.
    
        Raises:
            FlagException: If an unexpected flag is encountered or the log flag is missing.
        """
    
        result = {}
    
        try:
            for i in range(0, len(command_line_flags), 2):
                flag = command_line_flags[i]
                if (flag not in EXCEPTED_FLAGS):
                    raise FlagException()
    
                if (flag == EXCEPTED_FLAGS[0]):
                    result["log"] = command_line_flags[i + 1]
                elif (flag == EXCEPTED_FLAGS[1]):
                    result["system"] = command_line_flags[i + 1]
                elif (flag == EXCEPTED_FLAGS[2]):
                    result["json_output"] = command_line_flags[i + 1]
                else:
                    result["system_output"] = command_line_flags[i + 1]
        except IndexError:
            raise FlagException()
    
        # must have log file flag
        if "log" not in result:
            raise FlagException()
    
        return result
    
    def main(flags: list[str]):
        try:
            flags.remove(flags[0])
            files = flags_to_dict(flags)
    
            system = create_system("system" in files, files.get("system"))
            parse_log_file(files["log"], system)
            export(system, "json_output" in files, files.get("json_output"), "system_output" in files, files.get("system_output"))
        except FlagException:
            print("Usage: python3 matamazon.py -l < matamazon_log > -s < matamazon_system > -o <output_file> -os <out_matamazon_system>", file=sys.stderr)
            exit()
        except:
            print("The matamazon script has encountered an error", file=sys.stderr)
            exit()
    
    if __name__ == "__main__":
        main(sys.argv)