הערה (קובץ קוד)
קובץ זה הוא קובץ קוד מסוג 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)