From b34c5fd3cdd9d8ad3b7ba6e409655dd70837cabd Mon Sep 17 00:00:00 2001 From: nafisalawalidris Date: Sat, 31 Aug 2024 03:22:17 +0100 Subject: [PATCH] updated --- .github/workflows/ci.yml | 25 +++++ app/crud.py | 68 ++++++++++++ app/database.py | 115 ++++++++++++-------- app/main.py | 221 +++++++-------------------------------- app/models.py | 6 +- app/routes.py | 86 +++++++++++++++ app/schemas.py | 13 ++- setup.py | 0 test_main.py | 52 --------- tests/__init__.py | 0 tests/test_main.py | 57 ++++++++++ 11 files changed, 354 insertions(+), 289 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 app/crud.py create mode 100644 app/routes.py create mode 100644 setup.py delete mode 100644 test_main.py create mode 100644 tests/__init__.py create mode 100644 tests/test_main.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..95bfc9e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: | + pytest diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000..abc06d3 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,68 @@ +from sqlalchemy.orm import Session +from .models import BitcoinPrice +import requests + +def get_prices(db: Session, skip: int = 0, limit: int = 10): + return db.query(BitcoinPrice).offset(skip).limit(limit).all() + +def get_prices_by_year(db: Session, year: int): + start_date = f'{year}-01-01' + end_date = f'{year}-12-31' + return db.query(BitcoinPrice).filter(BitcoinPrice.date.between(start_date, end_date)).all() + +def get_prices_by_halving(db: Session, halving_number: int): + halving_periods = { + 1: ('2012-11-28', '2016-07-09'), + 2: ('2016-07-10', '2020-05-11'), + 3: ('2020-05-12', '2024-04-30'), + 4: ('2024-05-01', '2099-12-31') # Assumes the fourth period extends to a future date + } + + start_date, end_date = halving_periods.get(halving_number, (None, None)) + + if start_date is None or end_date is None: + return [] # Consider raising an HTTPException or returning an appropriate error message + + return db.query(BitcoinPrice).filter( + BitcoinPrice.date >= start_date, + BitcoinPrice.date <= end_date + ).all() + +def get_prices_across_halvings(db: Session): + return db.query(BitcoinPrice).all() + +def get_bybit_prices(): + try: + response = requests.get('https://api.bybit.com/v2/public/tickers') + response.raise_for_status() # Raises an HTTPError for bad responses + data = response.json() + return data.get('result', []) + except requests.RequestException as e: + # Log the error or handle it as needed + return {"error": str(e)} + +def get_binance_prices(): + try: + response = requests.get('https://api.binance.com/api/v3/ticker/price') + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + +def get_kraken_prices(): + try: + response = requests.get('https://api.kraken.com/0/public/Ticker') + response.raise_for_status() + data = response.json() + return data.get('result', {}) + except requests.RequestException as e: + return {"error": str(e)} + +def get_yahoo_prices(): + try: + response = requests.get('https://query1.finance.yahoo.com/v7/finance/quote?symbols=BTC-USD') + response.raise_for_status() + data = response.json() + return data.get('quoteResponse', {}).get('result', []) + except requests.RequestException as e: + return {"error": str(e)} diff --git a/app/database.py b/app/database.py index c453136..ccdfcf5 100644 --- a/app/database.py +++ b/app/database.py @@ -1,58 +1,87 @@ +# Import necessary libraries from SQLAlchemy +from sqlalchemy import create_engine, Column, Date, Float, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine -from dotenv import load_dotenv -import os -import logging - -# Load environment variables from .env file -load_dotenv() - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Get environment variables for database connection -POSTGRES_USER = os.getenv("POSTGRES_USER", "postgres") -POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "Feenah413") -POSTGRES_DB = os.getenv("POSTGRES_DB", "Bitcoin_Prices_Database") -POSTGRES_HOST = os.getenv("POSTGRES_HOST", "localhost") -POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") - -# Construct the database URL -SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" - -# Create the SQLAlchemy engine -try: - engine = create_engine(SQLALCHEMY_DATABASE_URL) - logger.info("Database engine created successfully.") -except Exception as e: - logger.error(f"Failed to create database engine: {e}") - raise - -# Base class for declarative models -Base = declarative_base() +import pandas as pd + +# Define the database URL. +# Replace 'username', 'password', 'localhost', and 'dbname' with your PostgreSQL credentials. +DATABASE_URL = "postgresql://postgres:Feenah413@localhost/Bitcoin_Prices_Database" + +# Create the SQLAlchemy engine. +# The engine is responsible for connecting to the database and executing SQL commands. +# We use 'create_engine' to initialize the connection to the PostgreSQL database. +engine = create_engine(DATABASE_URL, echo=True) # echo=True logs all the SQL commands -# Configure the session factory +# Create a SessionLocal class that will serve as a factory for creating new Session objects. +# Sessions are used to interact with the database and perform CRUD operations. SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -# Dependency for database session +# Create a base class for our models. +# All model classes will inherit from this base class. +# The base class is used to create the database schema and manage the metadata. +Base = declarative_base() + +# Dependency for getting a database session. +# This function will be used in FastAPI endpoints to get a session for interacting with the database. def get_db(): + # Create a new session. db = SessionLocal() try: + # Yield the session object to be used in the request. yield db finally: + # Close the session when done. db.close() -# Function to test the database connection -def test_connection(): +# Load the dataset from a CSV file +# Use raw string (r'path') or double backslashes for Windows paths +df = pd.read_csv(r"C:\Users\USER\Downloads\Bitcoin-Price-Analysis-API\data\BTC-USD Yahoo Finance - Max Yrs.csv", parse_dates=['Date']) + +# Check for missing values +print(df.isnull().sum()) + +# Drop rows with missing values (if necessary) +df = df.dropna() + +# Convert columns to appropriate data types +df['Date'] = pd.to_datetime(df['Date']) # Ensure 'Date' is in datetime format +df['Volume'] = df['Volume'].astype(int) # Convert 'Volume' to integer + +class BitcoinPrice(Base): + __tablename__ = 'bitcoin_prices' + + date = Column(Date, primary_key=True) + open = Column(Float) + high = Column(Float) + low = Column(Float) + close = Column(Float) + adj_close = Column(Float) + volume = Column(Integer) + +# Create the table +Base.metadata.create_all(bind=engine) + +def insert_data(df): + session = SessionLocal() try: - with engine.connect() as connection: - logger.info("Connected to the database successfully.") + for _, row in df.iterrows(): + price = BitcoinPrice( + date=row['Date'], + open=row['Open'], + high=row['High'], + low=row['Low'], + close=row['Close'], + adj_close=row['Adj Close'], + volume=row['Volume'] + ) + session.add(price) + session.commit() except Exception as e: - logger.error(f"Failed to connect to the database: {e}") - raise + session.rollback() + print(f"Error: {e}") + finally: + session.close() -if __name__ == "__main__": - # Test the database connection - test_connection() +# Insert the data +insert_data(df) diff --git a/app/main.py b/app/main.py index c6509fc..cbd2e37 100644 --- a/app/main.py +++ b/app/main.py @@ -1,59 +1,13 @@ +from fastapi import FastAPI, Depends, HTTPException +from sqlalchemy.orm import Session from typing import List -from fastapi import FastAPI, HTTPException, Depends -from pydantic import BaseModel -from sqlalchemy import create_engine, Column, Float, Date -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, Session -import os -import requests -import yfinance as yf -import logging -import pandas as pd -from datetime import date +from . import crud, schemas +from .database import SessionLocal +from fastapi import FastAPI +from app.schemas import Price, PriceList -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Environment variables -POSTGRES_USER = os.getenv("POSTGRES_USER", "postgres") -POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "Feenah413") -POSTGRES_DB = os.getenv("POSTGRES_DB", "Bitcoin_Prices_Database") -POSTGRES_HOST = os.getenv("POSTGRES_HOST", "localhost") -POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") - -SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" - -# SQLAlchemy setup -engine = create_engine(SQLALCHEMY_DATABASE_URL) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() - -# Create FastAPI app instance app = FastAPI() -# Define Pydantic models -class BitcoinPrice(BaseModel): - date: str - open: float - high: float - low: float - close: float - adj_close: float - volume: float - -# Define database models -class BitcoinPriceDB(Base): - __tablename__ = "bitcoin_prices" - - date = Column(Date, primary_key=True) - open = Column(Float) - high = Column(Float) - low = Column(Float) - close = Column(Float) - adj_close = Column(Float) - volume = Column(Float) - # Dependency to get the database session def get_db(): db = SessionLocal() @@ -62,146 +16,41 @@ def get_db(): finally: db.close() -def load_csv_to_postgresql(file_path: str): - try: - df = pd.read_csv(file_path) - df.to_sql('bitcoin_prices', con=engine, if_exists='append', index=False) - print("CSV data successfully loaded into the database.") - except Exception as e: - print(f"Failed to load CSV data into the database: {e}") - -@app.on_event("startup") -async def startup_event(): - print("Starting up...") - # Optionally load CSV data on startup - file_path = "C:\\Users\\USER\\Downloads\\Bitcoin-Price-Analysis-API\\data\\BTC-USD Yahoo Finance - Max Yrs.csv" - load_csv_to_postgresql(file_path) - try: - # Create the tables in the database if they do not exist - Base.metadata.create_all(bind=engine) - except Exception as e: - logger.error(f"Failed to connect to the database: {e}") - raise HTTPException(status_code=500, detail="Database connection error") - -# Define endpoints -@app.get("/prices/", response_model=List[BitcoinPrice]) -def get_all_prices(db: Session = Depends(get_db)): - logger.info("Endpoint /prices/ was accessed") - prices = db.query(BitcoinPriceDB).all() +@app.get("/prices/", response_model=List[schemas.Price]) +def get_all_prices(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + prices = crud.get_prices(db, skip=skip, limit=limit) return prices -@app.get("/prices/{year}", response_model=List[BitcoinPrice]) +@app.get("/prices/{year}", response_model=List[schemas.Price]) def get_prices_by_year(year: int, db: Session = Depends(get_db)): - logger.info(f"Endpoint /prices/{year} was accessed") - if year < 0: - raise HTTPException(status_code=400, detail="Year must be a positive integer") - start_date = f"{year}-01-01" - end_date = f"{year}-12-31" - prices_by_year = db.query(BitcoinPriceDB).filter(BitcoinPriceDB.date.between(start_date, end_date)).all() - return prices_by_year + prices = crud.get_prices_by_year(db, year=year) + if not prices: + raise HTTPException(status_code=404, detail="No prices found for the given year") + return prices -@app.get("/prices/halving/{halving_number}", response_model=List[BitcoinPrice]) +@app.get("/prices/halving/{halving_number}", response_model=List[schemas.Price]) def get_prices_by_halving(halving_number: int, db: Session = Depends(get_db)): - logger.info(f"Endpoint /prices/halving/{halving_number} was accessed") - halving_dates = { - 1: "2012-11-28", - 2: "2016-07-09", - 3: "2020-05-11", - 4: "2024-04-20", - } - if halving_number not in halving_dates: - raise HTTPException(status_code=400, detail="Invalid halving number") - halving_date = halving_dates[halving_number] - prices_by_halving = db.query(BitcoinPriceDB).filter(BitcoinPriceDB.date >= halving_date).all() - return prices_by_halving - -@app.get("/prices/halvings", response_model=List[BitcoinPrice]) -def get_prices_across_halvings(db: Session = Depends(get_db)): - logger.info("Endpoint /prices/halvings was accessed") - halving_dates = [ - "2012-11-28", - "2016-07-09", - "2020-05-11", - "2024-04-20" - ] - prices_across_halvings = [] - for date in halving_dates: - prices = db.query(BitcoinPriceDB).filter(BitcoinPriceDB.date >= date).all() - prices_across_halvings.extend(prices) - return prices_across_halvings - -@app.get("/prices/bybit", response_model=BitcoinPrice) -def get_bybit_price(): - logger.info("Endpoint /prices/bybit was accessed") - url = "https://api.bybit.com/v2/public/tickers?symbol=BTCUSD" - data = fetch_price_from_api(url) - price_data = data['result'][0] - bybit_price = BitcoinPrice( - date=date.today().strftime("%Y-%m-%d"), # Use the current date - open=float(price_data['last_price']), - high=float(price_data['last_price']), - low=float(price_data['last_price']), - close=float(price_data['last_price']), - adj_close=float(price_data['last_price']), - volume=float(price_data['volume_24h']) - ) - return bybit_price - -@app.get("/prices/binance", response_model=BitcoinPrice) -def get_binance_price(): - logger.info("Endpoint /prices/binance was accessed") - url = "https://api.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT" - data = fetch_price_from_api(url) - binance_price = BitcoinPrice( - date=date.today().strftime("%Y-%m-%d"), # Use the current date - open=float(data['openPrice']), - high=float(data['highPrice']), - low=float(data['lowPrice']), - close=float(data['lastPrice']), - adj_close=float(data['lastPrice']), - volume=float(data['volume']) - ) - return binance_price + prices = crud.get_prices_by_halving(db, halving_number=halving_number) + if not prices: + raise HTTPException(status_code=404, detail="No prices found for the given halving period") + return prices -@app.get("/prices/kraken", response_model=BitcoinPrice) -def get_kraken_price(): - logger.info("Endpoint /prices/kraken was accessed") - url = "https://api.kraken.com/0/public/Ticker?pair=XBTUSD" - data = fetch_price_from_api(url) - price_data = data['result']['XXBTZUSD'] - kraken_price = BitcoinPrice( - date=date.today().strftime("%Y-%m-%d"), # Use the current date - open=float(price_data['o'][0]), - high=float(price_data['h'][0]), - low=float(price_data['l'][0]), - close=float(price_data['c'][0]), - adj_close=float(price_data['c'][0]), - volume=float(price_data['v'][0]) - ) - return kraken_price +@app.get("/prices/bybit") +def get_bybit_prices_endpoint(): + prices = crud.get_bybit_prices() + return prices -@app.get("/prices/yahoo", response_model=List[BitcoinPrice]) -def get_yahoo_prices(): - logger.info("Endpoint /prices/yahoo was accessed") - data = yf.download("BTC-USD", period="1y", interval="1d") - yahoo_prices = [] - for index, row in data.iterrows(): - yahoo_prices.append(BitcoinPrice( - date=index.strftime("%Y-%m-%d"), - open=row['Open'], - high=row['High'], - low=row['Low'], - close=row['Close'], - adj_close=row['Adj Close'], - volume=row['Volume'] - )) - return yahoo_prices +@app.get("/prices/binance") +def get_binance_prices_endpoint(): + prices = crud.get_binance_prices() + return prices -def fetch_price_from_api(url: str): - response = requests.get(url) - response.raise_for_status() - return response.json() +@app.get("/prices/kraken") +def get_kraken_prices_endpoint(): + prices = crud.get_kraken_prices() + return prices -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) +@app.get("/prices/yahoo") +def get_yahoo_prices_endpoint(): + prices = crud.get_yahoo_prices() + return prices diff --git a/app/models.py b/app/models.py index 2c2c444..6f9140d 100644 --- a/app/models.py +++ b/app/models.py @@ -2,7 +2,6 @@ from sqlalchemy.ext.declarative import declarative_base from .database import Base # Import Base from your database setup file - Base = declarative_base() # Define a SQLAlchemy model for Bitcoin price data @@ -15,7 +14,7 @@ class BitcoinPrice(Base): low = Column(Float, nullable=False) close = Column(Float, nullable=False) adj_close = Column(Float, nullable=False) - volume = Column(Float, nullable=False) + volume = Column(Float, nullable=False) # Consider Integer if volume is always a whole number # Creating an index on 'date' for faster queries __table_args__ = ( @@ -23,4 +22,5 @@ class BitcoinPrice(Base): ) def __repr__(self): - return f"" + return (f"") diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..23784ea --- /dev/null +++ b/app/routes.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter, HTTPException, Depends +from typing import List +from sqlalchemy.orm import Session +from app.models import Price +from app.database import get_db +from app.crud import ( + get_all_prices, + get_price_by_year, + get_prices_by_halving, + get_prices_across_halvings, + get_bybit_price, + get_binance_price, + get_kraken_price, + get_yahoo_price +) + +# Create the API Router +router = APIRouter() + +@router.get("/prices/", response_model=List[Price]) +def read_all_prices(db: Session = Depends(get_db)): + """ + Retrieve all Bitcoin price records. + """ + prices = get_all_prices(db) + return prices + +@router.get("/prices/year/{year}", response_model=List[Price]) +def read_prices_by_year(year: int, db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records for a specific year. + """ + prices = get_price_by_year(db, year) + if not prices: + raise HTTPException(status_code=404, detail="Prices not found for the specified year") + return prices + +@router.get("/prices/halving/{halving_number}", response_model=List[Price]) +def read_prices_by_halving(halving_number: int, db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records for a specific halving period. + """ + prices = get_prices_by_halving(db, halving_number) + if not prices: + raise HTTPException(status_code=404, detail="Prices not found for the specified halving period") + return prices + +@router.get("/prices/halvings", response_model=List[Price]) +def read_prices_across_halvings(db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records across all halving periods. + """ + prices = get_prices_across_halvings(db) + return prices + +@router.get("/prices/bybit/", response_model=List[Price]) +def read_bybit_prices(db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records from Bybit. + """ + prices = get_bybit_price() + return prices + +@router.get("/prices/binance/", response_model=List[Price]) +def read_binance_prices(db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records from Binance. + """ + prices = get_binance_price() + return prices + +@router.get("/prices/kraken/", response_model=List[Price]) +def read_kraken_prices(db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records from Kraken. + """ + prices = get_kraken_price() + return prices + +@router.get("/prices/yahoo/", response_model=List[Price]) +def read_yahoo_prices(db: Session = Depends(get_db)): + """ + Retrieve Bitcoin price records from Yahoo Finance. + """ + prices = get_yahoo_price() + return prices diff --git a/app/schemas.py b/app/schemas.py index 0fe6f8a..3234857 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,8 +1,9 @@ +from typing import List from pydantic import BaseModel from datetime import date +from app.schemas import Price -# Define Pydantic models for request and response handling -class BitcoinPriceBase(BaseModel): +class PriceBase(BaseModel): date: date open: float high: float @@ -14,9 +15,11 @@ class BitcoinPriceBase(BaseModel): class Config: orm_mode = True -class BitcoinPriceCreate(BitcoinPriceBase): - pass +class Price(PriceBase): + id: int + +class PriceList(BaseModel): + prices: List[Price] -class BitcoinPriceResponse(BitcoinPriceBase): class Config: orm_mode = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/test_main.py b/test_main.py deleted file mode 100644 index 0d1c9af..0000000 --- a/test_main.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from main import app - -client = TestClient(app) - -def test_get_all_prices(): - response = client.get("/prices/") - assert response.status_code == 200 - assert isinstance(response.json(), list) # Ensure response is a list - if response.json(): # Check only if there's any data returned - assert "id" in response.json()[0] - assert "date" in response.json()[0] - -def test_get_prices_by_year(): - response = client.get("/prices/2023") - assert response.status_code in [200, 404] # Can be empty or filled - if response.status_code == 200: - assert isinstance(response.json(), list) - assert "date" in response.json()[0] - -def test_get_prices_by_invalid_year(): - response = client.get("/prices/-2023") - assert response.status_code == 400 - -def test_get_prices_by_halving(): - response = client.get("/prices/halving/1") - assert response.status_code in [200, 404] # Can be empty or filled - -def test_get_prices_invalid_halving(): - response = client.get("/prices/halving/5") - assert response.status_code == 400 - -def test_get_bybit_price(): - response = client.get("/prices/bybit") - assert response.status_code in [200, 500] # Ensure status is either okay or error - -def test_get_binance_price(): - response = client.get("/prices/binance") - assert response.status_code in [200, 500] - -def test_get_kraken_price(): - response = client.get("/prices/kraken") - assert response.status_code in [200, 500] - -def test_get_yahoo_prices(): - response = client.get("/prices/yahoo") - assert response.status_code in [200, 404] - if response.status_code == 200: - assert isinstance(response.json(), list) - if response.json(): # Check if data exists - assert "date" in response.json()[0] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..324e69a --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,57 @@ +# Testing with Pytest for the API endpoints + +# test_main.py + +import pytest +from httpx import AsyncClient +from fastapi import FastAPI +from app.main import app # Import your FastAPI application + +# Fixture to provide an HTTP client for testing +@pytest.fixture() +async def client(): + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + +# Test the endpoint to get all prices +@pytest.mark.asyncio +async def test_get_all_prices(client): + response = await client.get("/prices/") + assert response.status_code == 200 + assert "prices" in response.json() + # Add more assertions based on your expected response + +# Test the endpoint to get prices by year +@pytest.mark.asyncio +async def test_get_prices_by_year(client): + year = 2023 + response = await client.get(f"/prices/{year}") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) # Ensure response is a list + # Add more assertions to check data contents + +# Test the endpoint to get prices by halving period +@pytest.mark.asyncio +async def test_get_prices_by_halving(client): + halving_number = 3 + response = await client.get(f"/prices/halving/{halving_number}") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) # Ensure response is a list + # Add more assertions to check data contents + +# Test the endpoint to get prices across all halvings +@pytest.mark.asyncio +async def test_get_prices_across_halvings(client): + response = await client.get("/prices/halvings") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) # Ensure response is a list + # Add more assertions to check data contents + +# Test for 404 response when accessing a non-existent endpoint +@pytest.mark.asyncio +async def test_get_non_existent_endpoint(client): + response = await client.get("/non-existent-endpoint") + assert response.status_code == 404