To solve this optimization problem, we first need to define our variables and constraints. Let's denote the number of plush toys as \(P\) and the number of action figures as \(A\).

The objective is to maximize profit. The profit per plush toy is $4, and the profit per action figure is $4.50. So, the total profit can be represented as \(4P + 4.5A\).

We have two main constraints based on the time available for assembly and packaging:

1. **Assembly Time Constraint**: Each plush toy takes 20 minutes of assembly, and each action figure takes 15 minutes of assembly. The shop has 1200 minutes available for assembly. This constraint can be represented as \(20P + 15A \leq 1200\).

2. **Packaging Time Constraint**: Each plush toy takes 4 minutes of packaging, and each action figure takes 5 minutes of packaging. The shop has 900 minutes available for packaging. This constraint can be represented as \(4P + 5A \leq 900\).

Additionally, we know that the number of plush toys (\(P\)) and action figures (\(A\)) cannot be negative because you cannot produce a negative quantity of items. So, we have \(P \geq 0\) and \(A \geq 0\).

Now, let's translate these constraints into Gurobi code in Python to find the optimal production quantities that maximize profit.

```python
from gurobipy import *

# Create a model
m = Model("Toy_Shop_Optimization")

# Define variables
P = m.addVar(vtype=GRB.CONTINUOUS, name="Plush_Toys", lb=0)
A = m.addVar(vtype=GRB.CONTINUOUS, name="Action_Figures", lb=0)

# Set the objective function
m.setObjective(4*P + 4.5*A, GRB.MAXIMIZE)

# Add constraints
m.addConstr(20*P + 15*A <= 1200, "Assembly_Time")
m.addConstr(4*P + 5*A <= 900, "Packaging_Time")

# Optimize the model
m.optimize()

# Print results
if m.status == GRB.OPTIMAL:
    print("Optimal solution found:")
    print(f"Plush Toys: {P.x}")
    print(f"Action Figures: {A.x}")
    print(f"Max Profit: ${4*P.x + 4.5*A.x:.2f}")
else:
    print("No optimal solution found")
```