diff --git a/.coverage b/.coverage
new file mode 100644
index 0000000..2923e05
Binary files /dev/null and b/.coverage differ
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..57a3b94
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install uv
+ run: |
+ pip install uv
+ uv --version
+ - name: Create Virtual Environment
+ run: uv venv
+ - name: Install dependencies
+ run: uv pip install -e ".[dev]"
+ - name: Run tests with coverage
+ run: uv run pytest --cov=src --cov-fail-under=80
+ env:
+ PYTHONPATH: .
diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md
index 5f7dabf..fa57b48 100644
--- a/docs/BACKLOG.md
+++ b/docs/BACKLOG.md
@@ -1,18 +1,28 @@
-# Backlog da Sprint - Fluxo de Carrinho
+# Backlog do Produto - TechShop
-## User Stories
+Este documento rastreia todas as User Stories planejadas para o projeto TechShop.
-1. **US001 - Adicionar item ao carrinho:**
- * **Como um** usuário,
- * **Eu quero** adicionar um produto ao meu carrinho de compras a partir da página de detalhes do produto,
- * **Para que** eu possa comprá-lo posteriormente.
+## MVP - Carrinho e Checkout
-2. **US002 - Visualizar total do carrinho:**
- * **Como um** usuário,
- * **Eu quero** ver o valor total dos itens no meu carrinho,
- * **Para que** eu saiba o custo total da minha compra antes de ir para o checkout.
+### Gerenciamento de Carrinho
+- **US1:** Como cliente, quero poder adicionar produtos ao meu carrinho de compras para que eu possa comprá-los mais tarde. ([Issue #11](https://github.com/AxelPCG/techshop/issues/11))
+- **US2:** Como cliente, quero poder remover produtos do meu carrinho de compras para ajustar meu pedido. ([Issue #12](https://github.com/AxelPCG/techshop/issues/12))
+- **US3:** Como cliente, quero poder visualizar os produtos no meu carrinho para revisar meu pedido antes de finalizar a compra. ([Issue #13](https://github.com/AxelPCG/techshop/issues/13))
-3. **US003 - Remover item do carrinho:**
- * **Como um** usuário,
- * **Eu quero** remover um item do meu carrinho de compras,
- * **Para que** eu possa gerenciar os produtos que desejo comprar.
+### Visualização de Produtos
+- **US6:** Como cliente, quero poder navegar por uma lista de produtos para descobrir o que está disponível na loja. ([Issue #16](https://github.com/AxelPCG/techshop/issues/16))
+- **US7:** Como cliente, quero poder ver os detalhes de um produto específico para tomar uma decisão de compra informada. ([Issue #17](https://github.com/AxelPCG/techshop/issues/17))
+
+### Checkout
+- **US4:** Como cliente, quero poder iniciar o processo de checkout a partir do meu carrinho de compras para finalizar minha compra. ([Issue #14](https://github.com/AxelPCG/techshop/issues/14))
+- **US5:** Como cliente, quero poder inserir minhas informações de envio e pagamento durante o checkout para que o pedido possa ser processado e entregue. ([Issue #15](https://github.com/AxelPCG/techshop/issues/15))
+
+## Próximos Passos - Autenticação e Perfis
+
+### Autenticação
+- **US8:** Como um novo usuário, quero poder me cadastrar na plataforma usando e-mail e senha para ter uma conta pessoal. ([Issue #7](https://github.com/AxelPCG/techshop/issues/7))
+- **US9:** Como um usuário cadastrado, quero poder fazer login no sistema para acessar minha conta e histórico. ([Issue #8](https://github.com/AxelPCG/techshop/issues/8))
+- **US10:** Como um usuário logado, quero poder fazer logout para proteger minha conta em dispositivos compartilhados. ([Issue #9](https://github.com/AxelPCG/techshop/issues/9))
+
+### Perfil de Usuário
+- **US11:** Como um usuário logado, quero ter uma página de perfil onde posso ver e editar minhas informações básicas (como nome e endereço de entrega). ([Issue #10](https://github.com/AxelPCG/techshop/issues/10))
diff --git a/docs/PRD_AUTH.md b/docs/PRD_AUTH.md
new file mode 100644
index 0000000..80641bc
--- /dev/null
+++ b/docs/PRD_AUTH.md
@@ -0,0 +1,14 @@
+### Product Requirements Document: Autenticação e Perfis de Usuário
+
+#### 1. Objetivo
+Implementar um sistema de autenticação seguro que permita aos usuários criar contas, fazer login/logout e gerenciar um perfil básico. Isso é fundamental para personalizar a experiência de compra e permitir o rastreamento de pedidos.
+
+#### 2. Requisitos Funcionais
+* **US8:** Como um novo usuário, quero poder me cadastrar na plataforma usando e-mail and senha para ter uma conta pessoal.
+* **US9:** Como um usuário cadastrado, quero poder fazer login no sistema para acessar minha conta e histórico.
+* **US10:** Como um usuário logado, quero poder fazer logout para proteger minha conta em dispositivos compartilhados.
+* **US11:** Como um usuário logado, quero ter uma página de perfil onde posso ver e editar minhas informações básicas (como nome e endereço de entrega).
+
+#### 3. Requisitos Não-Funcionais
+* **Segurança:** Senhas devem ser armazenadas de forma segura (hashed). O sistema deve ter proteção contra ataques comuns (ex: brute-force).
+* **Usabilidade:** O processo de login e cadastro deve ser simples e intuitivo.
diff --git a/pyproject.toml b/pyproject.toml
index 08724b4..078b514 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,15 +6,20 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.136.1",
+ "pandas>=3.0.3",
"pydantic>=2.13.4",
"uvicorn>=0.46.0",
]
-[dependency-groups]
+[project.optional-dependencies]
dev = [
"mypy>=2.0.0",
"playwright>=1.59.0",
"pytest>=9.0.3",
"pytest-cov>=7.1.0",
"ruff>=0.15.12",
+ "httpx>=0.27.0",
]
+
+[tool.pytest.ini_options]
+pythonpath = "."
diff --git a/src/checkout.py b/src/checkout.py
new file mode 100644
index 0000000..a3de3cc
--- /dev/null
+++ b/src/checkout.py
@@ -0,0 +1,113 @@
+# checkout.py - Refatorado
+
+from typing import List, Dict, Any
+from .models import CartItem, UserData
+
+# --- Protocolos para Injeção de Dependência ---
+
+class StockValidatorProtocol:
+ def validate(self, items: List[CartItem]) -> bool: ...
+
+class ShippingServiceProtocol:
+ def calculate(self, items: List[CartItem]) -> float: ...
+
+class DiscountServiceProtocol:
+ def calculate(self, total_value: float, user: UserData) -> float: ...
+
+class PaymentServiceProtocol:
+ def process(self, user: UserData, final_amount: float) -> Dict[str, Any]: ...
+
+# --- Implementações Concretas dos Serviços ---
+
+class StockValidator:
+ """Valida o estoque dos itens (implementação mock)."""
+ def validate(self, items: List[CartItem]) -> bool:
+ print("--- Validando estoque... ---")
+ for item in items:
+ # Lógica de estoque mockada
+ mock_stock = 10
+ if item.quantity > mock_stock:
+ print(f"ERRO: Estoque insuficiente para o produto {item.product.id}.")
+ raise ValueError(f"Estoque insuficiente para {item.product.name}")
+ print("--- Estoque OK. ---")
+ return True
+
+class ShippingService:
+ """Calcula o frete (valor fixo para este exemplo)."""
+ _SHIPPING_FEE = 15.50
+
+ def calculate(self, items: List[CartItem]) -> float:
+ print(f"--- Calculando frete fixo de R$ {self._SHIPPING_FEE}... ---")
+ return self._SHIPPING_FEE
+
+class DiscountService:
+ """Calcula descontos com base no valor total e no tipo de usuário."""
+ def calculate(self, total_value: float, user: UserData) -> float:
+ print("--- Calculando descontos... ---")
+ if total_value > 1000:
+ print("--- Aplicando desconto de 20% (compras > R$ 1000) ---")
+ return total_value * 0.80
+ if total_value > 500:
+ print("--- Aplicando desconto de 10% (compras > R$ 500) ---")
+ return total_value * 0.90
+ if user.is_vip:
+ print("--- Aplicando desconto de 15% (usuário VIP) ---")
+ return total_value * 0.85
+ return total_value
+
+class FakePaymentAPI:
+ """Simula uma API de pagamento externa."""
+ def charge(self, amount: float, user_id: int) -> Dict[str, Any]:
+ print(f"--- Processando pagamento de R$ {amount:.2f} para o usuário {user_id}... ---")
+ if 0 < amount < 9999:
+ print("--- Pagamento APROVADO (simulado) ---")
+ return {"status": "pagamento_aprovado", "transaction_id": "xyz123abc"}
+ else:
+ print("--- Pagamento RECUSADO (simulado) ---")
+ raise ConnectionError("Falha ao processar pagamento: valor inválido.")
+
+class PaymentService:
+ """Serviço que interage com a API de pagamento."""
+ def __init__(self, payment_api: FakePaymentAPI):
+ self.payment_api = payment_api
+
+ def process(self, user: UserData, final_amount: float) -> Dict[str, Any]:
+ return self.payment_api.charge(amount=final_amount, user_id=user.id)
+
+# --- Orquestrador do Checkout ---
+
+def process_checkout(
+ cart_items: List[CartItem],
+ user_data: UserData,
+ stock_validator: StockValidatorProtocol,
+ shipping_service: ShippingServiceProtocol,
+ discount_service: DiscountServiceProtocol,
+ payment_service: PaymentServiceProtocol,
+) -> Dict[str, Any]:
+ """
+ Orquestra o processo de checkout utilizando injeção de dependência.
+ """
+ print("--- Iniciando processo de checkout refatorado ---")
+
+ # 1. Validar estoque
+ stock_validator.validate(cart_items)
+
+ # 2. Calcular total inicial
+ subtotal = sum(item.product.price * item.quantity for item in cart_items)
+ print(f"Subtotal: R$ {subtotal:.2f}")
+
+ # 3. Calcular frete
+ shipping_cost = shipping_service.calculate(cart_items)
+ total_with_shipping = subtotal + shipping_cost
+ print(f"Total com frete: R$ {total_with_shipping:.2f}")
+
+ # 4. Aplicar descontos
+ final_amount = discount_service.calculate(total_with_shipping, user_data)
+ print(f"Valor final após descontos: R$ {final_amount:.2f}")
+
+ # 5. Processar pagamento
+ payment_result = payment_service.process(user_data, round(final_amount, 2))
+
+ print(f"--- Checkout finalizado! Resultado: {payment_result} ---")
+ return {"success": True, "payment_details": payment_result}
+
diff --git a/src/main.py b/src/main.py
index 18abb38..144f00b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,7 +1,102 @@
-from fastapi import FastAPI
+from fastapi import FastAPI, Depends, HTTPException
+from fastapi.staticfiles import StaticFiles
+from typing import List, Dict, Any
-app = FastAPI()
+from .models import CartItem, UserData, Product
+from .cart import ShoppingCart
+from .checkout import (
+ process_checkout,
+ StockValidator,
+ ShippingService,
+ DiscountService,
+ PaymentService,
+ FakePaymentAPI,
+)
+
+app = FastAPI(
+ title="TechShop API",
+ description="API para um e-commerce de tecnologia, com gerenciamento de carrinho e checkout.",
+ version="1.0.0",
+)
+
+# Monta a pasta 'static' para servir arquivos estáticos (nosso frontend)
+app.mount("/app", StaticFiles(directory="static", html=True), name="static")
+
+# --- Simulação de um "banco de dados" em memória para o carrinho ---
+# Em uma aplicação real, isso seria substituído por um banco de dados (Redis, PostgreSQL, etc.)
+shopping_cart_db: Dict[int, ShoppingCart] = {
+ 1: ShoppingCart() # Carrinho para o usuário com ID 1
+}
+
+# --- Injeção de Dependências (FastAPI Depends) ---
+
+def get_cart(user_id: int = 1) -> ShoppingCart:
+ """Retorna a instância do carrinho de compras para o usuário."""
+ cart = shopping_cart_db.get(user_id)
+ if not cart:
+ raise HTTPException(status_code=404, detail="Carrinho não encontrado para o usuário.")
+ return cart
+
+# Instâncias dos serviços que serão injetados
+stock_validator = StockValidator()
+shipping_service = ShippingService()
+discount_service = DiscountService()
+payment_api = FakePaymentAPI()
+payment_service = PaymentService(payment_api=payment_api)
+
+
+# --- Rotas da API ---
+
+@app.post("/cart/add", response_model=CartItem, tags=["Carrinho"])
+def add_to_cart(item: CartItem, cart: ShoppingCart = Depends(get_cart)):
+ """
+ Adiciona um item ao carrinho de compras do usuário.
+
+ Se o produto já existir no carrinho, a quantidade será somada.
+ """
+ cart.add_item(item.product, item.quantity)
+ return item
+
+@app.get("/cart", tags=["Carrinho"])
+def get_cart_details(cart: ShoppingCart = Depends(get_cart)):
+ """
+ Retorna os detalhes do carrinho de compras do usuário, incluindo
+ os itens, o subtotal e o total com descontos aplicados.
+ """
+ return {
+ "items": cart.items,
+ "subtotal": cart.calculate_total(),
+ "total_with_discount": cart.calculate_total_with_discount(),
+ }
+
+@app.post("/checkout", tags=["Checkout"])
+def perform_checkout(user_data: UserData, cart: ShoppingCart = Depends(get_cart)):
+ """
+ Processa o checkout do carrinho de compras.
+
+ Realiza a validação de estoque, cálculo de frete, aplicação de descontos
+ e processamento do pagamento.
+ """
+ if not cart.items:
+ raise HTTPException(status_code=400, detail="Carrinho está vazio.")
+
+ try:
+ result = process_checkout(
+ cart_items=cart.items,
+ user_data=user_data,
+ stock_validator=stock_validator,
+ shipping_service=shipping_service,
+ discount_service=discount_service,
+ payment_service=payment_service,
+ )
+ # Limpa o carrinho após checkout bem-sucedido
+ cart.items.clear()
+ return {"status": "Sucesso", "details": result}
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except ConnectionError as e:
+ raise HTTPException(status_code=503, detail=str(e))
+ except Exception as e:
+ # Captura genérica para outros erros inesperados
+ raise HTTPException(status_code=500, detail=f"Ocorreu um erro inesperado: {e}")
-@app.get("/")
-def read_root():
- return {"status": "ok"}
diff --git a/src/models.py b/src/models.py
index 38b930b..1214a54 100644
--- a/src/models.py
+++ b/src/models.py
@@ -8,3 +8,7 @@ class Product(BaseModel):
class CartItem(BaseModel):
product: Product
quantity: int
+
+class UserData(BaseModel):
+ id: int
+ is_vip: bool = False
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..7a51006
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+ TechShop - Frontend Simples
+
+
+
+
+ TechShop - Simulação de Frontend
+
+ Produtos
+ Adicionar Mouse Gamer (R$ 150,00)
+ Adicionar Teclado Mecânico (R$ 350,00)
+
+ Carrinho
+ Atualizar Carrinho
+
+
Clique em "Atualizar Carrinho" para ver os detalhes.
+
+
+ Checkout
+ Finalizar Compra
+
+
Aguardando finalização da compra...
+
+
+
+
+
+
diff --git a/tests/test_cart.py b/tests/test_cart.py
new file mode 100644
index 0000000..5588f8d
--- /dev/null
+++ b/tests/test_cart.py
@@ -0,0 +1,171 @@
+import pytest
+from src.models import Product
+from src.cart import ShoppingCart
+
+# Arrange: Fixtures para produtos
+@pytest.fixture
+def product_1():
+ """Retorna um produto de exemplo com preço 10.00."""
+ return Product(id=1, name="Test Product 1", price=10.00, category="Test")
+
+@pytest.fixture
+def product_2():
+ """Retorna um produto de exemplo com preço 50.00."""
+ return Product(id=2, name="Test Product 2", price=50.00, category="Test")
+
+@pytest.fixture
+def product_expensive():
+ """Retorna um produto caro para testar descontos."""
+ return Product(id=3, name="Expensive Product", price=600.00, category="Test")
+
+@pytest.fixture
+def product_very_expensive():
+ """Retorna um produto muito caro para testar descontos."""
+ return Product(id=4, name="Very Expensive Product", price=1100.00, category="Test")
+
+
+# --- Testes para a classe ShoppingCart ---
+
+def test_add_new_item_to_cart():
+ """
+ Testa se um novo item é adicionado corretamente ao carrinho.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ product = Product(id=1, name="Test", price=10.0, category="Test")
+ quantity = 2
+
+ # Act
+ cart.add_item(product, quantity)
+
+ # Assert
+ assert len(cart.items) == 1
+ assert cart.items[0].product == product
+ assert cart.items[0].quantity == quantity
+
+def test_add_existing_item_to_cart():
+ """
+ Testa se a quantidade de um item existente é atualizada corretamente.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ product = Product(id=1, name="Test", price=10.0, category="Test")
+ cart.add_item(product, 2)
+
+ # Act
+ cart.add_item(product, 3)
+
+ # Assert
+ assert len(cart.items) == 1
+ assert cart.items[0].quantity == 5
+
+def test_remove_item_from_cart(product_1):
+ """
+ Testa se um item é removido corretamente do carrinho.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_1, 1)
+
+ # Act
+ cart.remove_item(product_1.id)
+
+ # Assert
+ assert len(cart.items) == 0
+
+def test_calculate_total_empty_cart():
+ """
+ Testa o cálculo do total para um carrinho vazio (caso de borda).
+ """
+ # Arrange
+ cart = ShoppingCart()
+
+ # Act
+ total = cart.calculate_total()
+
+ # Assert
+ assert total == 0
+
+def test_calculate_total_with_items(product_1, product_2):
+ """
+ Testa o cálculo do total para um carrinho com múltiplos itens.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_1, 5) # 5 * 10.00 = 50.00
+ cart.add_item(product_2, 2) # 2 * 50.00 = 100.00
+
+ # Act
+ total = cart.calculate_total()
+
+ # Assert
+ assert total == 150.00
+
+def test_discount_no_discount_below_threshold(product_1):
+ """
+ Testa se nenhum desconto é aplicado para totais abaixo de 500.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_1, 49) # Total = 490.00
+
+ # Act
+ total_with_discount = cart.calculate_total_with_discount()
+
+ # Assert
+ assert total_with_discount == 490.00
+
+def test_discount_exact_threshold_500(product_1):
+ """
+ Testa o cálculo do desconto no limiar exato de 500 (caso de borda).
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_1, 50) # Total = 500.00
+
+ # Act
+ total_with_discount = cart.calculate_total_with_discount()
+
+ # Assert
+ assert total_with_discount == 500.00 # Sem desconto
+
+def test_discount_10_percent_just_above_500(product_expensive):
+ """
+ Testa o desconto de 10% para um total ligeiramente acima de 500.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_expensive, 1) # Total = 600.00
+
+ # Act
+ total_with_discount = cart.calculate_total_with_discount()
+
+ # Assert
+ assert total_with_discount == 600.00 * 0.90
+
+def test_discount_20_percent_just_above_1000(product_very_expensive):
+ """
+ Testa o desconto de 20% para um total ligeiramente acima de 1000.
+ """
+ # Arrange
+ cart = ShoppingCart()
+ cart.add_item(product_very_expensive, 1) # Total = 1100.00
+
+ # Act
+ total_with_discount = cart.calculate_total_with_discount()
+
+ # Assert
+ assert total_with_discount == 1100.00 * 0.80
+
+def test_discount_empty_cart(product_1):
+ """
+ Testa o cálculo de desconto para um carrinho vazio (caso de borda).
+ """
+ # Arrange
+ cart = ShoppingCart()
+
+ # Act
+ total_with_discount = cart.calculate_total_with_discount()
+
+ # Assert
+ assert total_with_discount == 0
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
new file mode 100644
index 0000000..b7a265a
--- /dev/null
+++ b/tests/test_checkout.py
@@ -0,0 +1,152 @@
+import pytest
+from unittest.mock import MagicMock, patch
+from src.checkout import (
+ StockValidator,
+ ShippingService,
+ DiscountService,
+ FakePaymentAPI,
+ PaymentService,
+ process_checkout,
+)
+from src.models import Product, CartItem, UserData
+
+# Fixtures
+@pytest.fixture
+def sample_product():
+ return Product(id=1, name="Sample Product", price=100.0, category="Electronics")
+
+@pytest.fixture
+def sample_cart_item(sample_product):
+ return CartItem(product=sample_product, quantity=2)
+
+@pytest.fixture
+def vip_user():
+ return UserData(id=1, name="VIP User", email="vip@test.com", is_vip=True)
+
+@pytest.fixture
+def regular_user():
+ return UserData(id=2, name="Regular User", email="regular@test.com", is_vip=False)
+
+# Testes para StockValidator
+def test_stock_validator_success(sample_cart_item):
+ validator = StockValidator()
+ assert validator.validate([sample_cart_item]) is True
+
+def test_stock_validator_insufficient_stock(sample_cart_item):
+ validator = StockValidator()
+ sample_cart_item.quantity = 11 # mock_stock is 10
+ with pytest.raises(ValueError, match="Estoque insuficiente para Sample Product"):
+ validator.validate([sample_cart_item])
+
+# Testes para ShippingService
+def test_shipping_service_calculate():
+ service = ShippingService()
+ assert service.calculate([]) == 15.50
+
+# Testes para DiscountService
+def test_discount_service_over_1000(regular_user):
+ service = DiscountService()
+ assert service.calculate(1200.0, regular_user) == 1200.0 * 0.80
+
+def test_discount_service_over_500(regular_user):
+ service = DiscountService()
+ assert service.calculate(600.0, regular_user) == 600.0 * 0.90
+
+def test_discount_service_vip_user(vip_user):
+ service = DiscountService()
+ assert service.calculate(400.0, vip_user) == 400.0 * 0.85
+
+def test_discount_service_no_discount(regular_user):
+ service = DiscountService()
+ assert service.calculate(400.0, regular_user) == 400.0
+
+# Testes para FakePaymentAPI e PaymentService
+def test_fake_payment_api_charge_success():
+ api = FakePaymentAPI()
+ result = api.charge(100.0, 1)
+ assert result["status"] == "pagamento_aprovado"
+
+def test_fake_payment_api_charge_failure():
+ api = FakePaymentAPI()
+ with pytest.raises(ConnectionError):
+ api.charge(10000.0, 1)
+
+def test_payment_service_process():
+ mock_api = MagicMock()
+ mock_api.charge.return_value = {"status": "success"}
+ service = PaymentService(payment_api=mock_api)
+ user = UserData(id=1, name="Test", email="test@test.com", is_vip=False)
+ result = service.process(user, 150.0)
+ mock_api.charge.assert_called_once_with(amount=150.0, user_id=1)
+ assert result == {"status": "success"}
+
+# Testes para o orquestrador process_checkout
+def test_process_checkout_success(sample_cart_item, regular_user):
+ # Mocks para os serviços
+ stock_validator = MagicMock()
+ shipping_service = MagicMock()
+ discount_service = MagicMock()
+ payment_service = MagicMock()
+
+ # Configuração dos retornos dos mocks
+ stock_validator.validate.return_value = True
+ shipping_service.calculate.return_value = 15.50
+ # subtotal (200) + shipping (15.50) = 215.50
+ discount_service.calculate.return_value = 215.50 # Sem desconto
+ payment_service.process.return_value = {"status": "pagamento_aprovado"}
+
+ result = process_checkout(
+ cart_items=[sample_cart_item],
+ user_data=regular_user,
+ stock_validator=stock_validator,
+ shipping_service=shipping_service,
+ discount_service=discount_service,
+ payment_service=payment_service,
+ )
+
+ # Asserts
+ stock_validator.validate.assert_called_once()
+ shipping_service.calculate.assert_called_once()
+ discount_service.calculate.assert_called_once()
+ payment_service.process.assert_called_once()
+ assert result["success"] is True
+ assert result["payment_details"]["status"] == "pagamento_aprovado"
+
+def test_process_checkout_stock_failure(sample_cart_item, regular_user):
+ stock_validator = MagicMock()
+ stock_validator.validate.side_effect = ValueError("Estoque insuficiente")
+
+ shipping_service = MagicMock()
+ discount_service = MagicMock()
+ payment_service = MagicMock()
+
+ with pytest.raises(ValueError, match="Estoque insuficiente"):
+ process_checkout(
+ cart_items=[sample_cart_item],
+ user_data=regular_user,
+ stock_validator=stock_validator,
+ shipping_service=shipping_service,
+ discount_service=discount_service,
+ payment_service=payment_service,
+ )
+
+def test_process_checkout_payment_failure(sample_cart_item, regular_user):
+ stock_validator = MagicMock()
+ shipping_service = MagicMock()
+ discount_service = MagicMock()
+ payment_service = MagicMock()
+
+ stock_validator.validate.return_value = True
+ shipping_service.calculate.return_value = 15.50
+ discount_service.calculate.return_value = 215.50
+ payment_service.process.side_effect = ConnectionError("Falha no pagamento")
+
+ with pytest.raises(ConnectionError, match="Falha no pagamento"):
+ process_checkout(
+ cart_items=[sample_cart_item],
+ user_data=regular_user,
+ stock_validator=stock_validator,
+ shipping_service=shipping_service,
+ discount_service=discount_service,
+ payment_service=payment_service,
+ )
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..d0912b2
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,107 @@
+import pytest
+from fastapi.testclient import TestClient
+from unittest.mock import patch, MagicMock
+from src.main import app, get_cart
+from src.models import Product, CartItem, UserData
+from src.cart import ShoppingCart
+
+client = TestClient(app)
+
+# Fixtures
+@pytest.fixture
+def sample_product():
+ return Product(id=1, name="Sample Product", price=100.0, category="Electronics")
+
+@pytest.fixture
+def sample_cart_item(sample_product):
+ return CartItem(product=sample_product, quantity=1)
+
+@pytest.fixture
+def mock_cart():
+ cart = ShoppingCart()
+ # Sobrescreve a dependência do carrinho para usar nosso mock
+ app.dependency_overrides[get_cart] = lambda: cart
+ return cart
+
+# Testes para a rota /cart/add
+def test_add_to_cart_success(mock_cart, sample_cart_item):
+ response = client.post("/cart/add", json=sample_cart_item.model_dump())
+ assert response.status_code == 200
+ assert len(mock_cart.items) == 1
+ assert mock_cart.items[0].quantity == 1
+
+# Testes para a rota /cart
+def test_get_cart_details_success(mock_cart, sample_cart_item):
+ mock_cart.add_item(sample_cart_item.product, sample_cart_item.quantity)
+ response = client.get("/cart")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["subtotal"] == 100.0
+ assert len(data["items"]) == 1
+
+# Testes para a rota /checkout
+def test_checkout_empty_cart(mock_cart):
+ response = client.post("/checkout", json=UserData(id=1, name="Test", email="a@a.com", is_vip=False).model_dump())
+ assert response.status_code == 400
+ assert "Carrinho está vazio" in response.json()["detail"]
+
+@patch('src.main.process_checkout')
+def test_checkout_success(mock_process_checkout, mock_cart, sample_cart_item):
+ # Arrange
+ mock_cart.add_item(sample_cart_item.product, 1)
+ user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False)
+
+ # Configura o mock para retornar um resultado de sucesso
+ mock_process_checkout.return_value = {"success": True, "payment_details": {"status": "pagamento_aprovado"}}
+
+ # Act
+ response = client.post("/checkout", json=user_data.model_dump())
+
+ # Assert
+ assert response.status_code == 200
+ assert response.json()["status"] == "Sucesso"
+ mock_process_checkout.assert_called_once()
+ # Verifica se o carrinho foi limpo
+ assert len(mock_cart.items) == 0
+
+@patch('src.main.process_checkout')
+def test_checkout_stock_failure(mock_process_checkout, mock_cart, sample_cart_item):
+ mock_cart.add_item(sample_cart_item.product, 1)
+ user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False)
+
+ mock_process_checkout.side_effect = ValueError("Estoque insuficiente")
+
+ response = client.post("/checkout", json=user_data.model_dump())
+
+ assert response.status_code == 400
+ assert "Estoque insuficiente" in response.json()["detail"]
+
+@patch('src.main.process_checkout')
+def test_checkout_payment_failure(mock_process_checkout, mock_cart, sample_cart_item):
+ mock_cart.add_item(sample_cart_item.product, 1)
+ user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False)
+
+ mock_process_checkout.side_effect = ConnectionError("Falha no pagamento")
+
+ response = client.post("/checkout", json=user_data.model_dump())
+
+ assert response.status_code == 503
+ assert "Falha no pagamento" in response.json()["detail"]
+
+@patch('src.main.process_checkout')
+def test_checkout_unexpected_error(mock_process_checkout, mock_cart, sample_cart_item):
+ mock_cart.add_item(sample_cart_item.product, 1)
+ user_data = UserData(id=1, name="Test User", email="test@user.com", is_vip=False)
+
+ mock_process_checkout.side_effect = Exception("Erro inesperado")
+
+ response = client.post("/checkout", json=user_data.model_dump())
+
+ assert response.status_code == 500
+ assert "Ocorreu um erro inesperado" in response.json()["detail"]
+
+# Limpa as substituições de dependência após os testes
+@pytest.fixture(autouse=True, scope="module")
+def cleanup():
+ yield
+ app.dependency_overrides.clear()
diff --git a/uv.lock b/uv.lock
index 04122c2..daf7e08 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,8 +2,15 @@ version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
- "python_full_version >= '3.15'",
- "python_full_version < '3.15'",
+ "python_full_version >= '3.15' and sys_platform == 'win32'",
+ "python_full_version >= '3.15' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version == '3.14.*' and sys_platform == 'win32'",
+ "python_full_version == '3.14.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[[package]]
@@ -75,6 +82,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" },
]
+[[package]]
+name = "certifi"
+version = "2026.4.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
+]
+
[[package]]
name = "click"
version = "8.3.3"
@@ -252,6 +268,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
[[package]]
name = "idna"
version = "3.14"
@@ -383,6 +427,67 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "numpy"
+version = "2.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
+ { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
+ { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
+ { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
+ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
+]
+
[[package]]
name = "packaging"
version = "26.2"
@@ -392,6 +497,58 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
+[[package]]
+name = "pandas"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" },
+ { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
+ { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
+ { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
+ { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
+ { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
+ { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
+ { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
+ { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
+ { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
+ { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
+ { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
+ { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
+ { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
+]
+
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -570,6 +727,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
[[package]]
name = "ruff"
version = "0.15.12"
@@ -595,6 +764,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
]
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
[[package]]
name = "starlette"
version = "1.0.0"
@@ -614,12 +792,14 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "fastapi" },
+ { name = "pandas" },
{ name = "pydantic" },
{ name = "uvicorn" },
]
-[package.dev-dependencies]
+[package.optional-dependencies]
dev = [
+ { name = "httpx" },
{ name = "mypy" },
{ name = "playwright" },
{ name = "pytest" },
@@ -630,18 +810,17 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.136.1" },
+ { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
+ { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.0.0" },
+ { name = "pandas", specifier = ">=3.0.3" },
+ { name = "playwright", marker = "extra == 'dev'", specifier = ">=1.59.0" },
{ name = "pydantic", specifier = ">=2.13.4" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" },
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.12" },
{ name = "uvicorn", specifier = ">=0.46.0" },
]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.0.0" },
- { name = "playwright", specifier = ">=1.59.0" },
- { name = "pytest", specifier = ">=9.0.3" },
- { name = "pytest-cov", specifier = ">=7.1.0" },
- { name = "ruff", specifier = ">=0.15.12" },
-]
+provides-extras = ["dev"]
[[package]]
name = "typing-extensions"
@@ -664,6 +843,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
+[[package]]
+name = "tzdata"
+version = "2026.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
+]
+
[[package]]
name = "uvicorn"
version = "0.46.0"