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

+ + + +

Carrinho

+ +
+

Clique em "Atualizar Carrinho" para ver os detalhes.

+
+ +

Checkout

+ +
+

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"