diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5167461 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python -m build + twine check dist/* + twine upload dist/* \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a439874 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pytest pytest-cov + pip install -e . + - name: Test with pytest + run: | + pytest --cov=dexpaprika_sdk tests/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e7f5a24 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include LICENSE +include README.md +include CHANGELOG.md +recursive-include examples *.py +recursive-include docs *.md *.py *.rst +global-exclude __pycache__ +global-exclude *.py[cod] +global-exclude *.so +global-exclude .DS_Store \ No newline at end of file diff --git a/README.md b/README.md index d81c5d4..2a1c7aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # DexPaprika Python SDK +[![PyPI version](https://badge.fury.io/py/dexpaprika-sdk.svg)](https://badge.fury.io/py/dexpaprika-sdk) +[![Python Version](https://img.shields.io/pypi/pyversions/dexpaprika-sdk)](https://pypi.org/project/dexpaprika-sdk/) +[![Tests](https://github.com/coinpaprika/dexpaprika-sdk-python/actions/workflows/tests.yml/badge.svg)](https://github.com/coinpaprika/dexpaprika-sdk-python/actions/workflows/tests.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + A Python client for the DexPaprika API. This SDK provides easy access to real-time data from decentralized exchanges across multiple blockchain networks. ## Features @@ -241,12 +246,47 @@ The SDK provides the following main components: - `SearchAPI`: Search for tokens, pools, and DEXes - `UtilsAPI`: Utility endpoints like global statistics +## Publishing + +For developers contributing to this package, here's how to publish a new version: + +1. Update the version in `dexpaprika_sdk/__init__.py` +2. Update the `CHANGELOG.md` +3. Create a new release in GitHub +4. GitHub Actions will automatically build and publish to PyPI + +## Development Setup + +```bash +# Clone the repository +git clone https://github.com/coinpaprika/dexpaprika-sdk-python.git +cd dexpaprika-sdk-python + +# Create a virtual environment (optional) +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install dev dependencies +pip install -e ".[dev]" +``` + +## Running Tests + +```bash +# Run tests with pytest +pytest + +# Run with coverage +pytest --cov=dexpaprika_sdk tests/ +``` + ## Resources - [Official Documentation](https://docs.dexpaprika.com) - Comprehensive API reference - [DexPaprika Website](https://dexpaprika.com) - Main product website - [CoinPaprika](https://coinpaprika.com) - Related cryptocurrency data platform - [Discord Community](https://discord.gg/DhJge5TUGM) - Get support and connect with other developers +- [PyPI Package](https://pypi.org/project/dexpaprika-sdk/) - Python package details ## License diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index 093ef51..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,95 +0,0 @@ -# SDK Enhancement Summary - -This document summarizes the changes made to implement caching and retry with backoff features in the DexPaprika SDK. - -## Files Modified - -1. **dexpaprika_sdk/client.py** - - Added retry with backoff mechanism to the request method - - Added configurable retry parameters (max_retries, backoff_times) - - Added method to determine if an error is retryable - - Added clear_cache method to clear the cache across all API services - -2. **dexpaprika_sdk/api/base.py** - - Added CacheEntry class to store data with expiration time - - Implemented TTL-based caching with different durations for different data types - - Added support for caching parameterized requests - - Added methods for cache key generation and TTL management - - Added cache clearing functionality - -3. **dexpaprika_sdk/__init__.py** - - Updated version number to 0.2.0 - -4. **setup.py** - - Updated version number to 0.2.0 - -5. **README.md** - - Added documentation for the new caching and retry features - - Added code examples demonstrating the new functionality - -## Files Created - -1. **examples/advanced_usage.py** - - Added demonstration of caching functionality - - Added demonstration of retry with backoff functionality - -2. **test_features.py** - - Added unit tests for caching behavior - - Added unit tests for retry behavior - -3. **CHANGELOG.md** - - Created changelog file to track version changes - - Added entries for versions 0.1.0 and 0.2.0 - -4. **docs/caching_and_retry.md** - - Created detailed documentation of the caching system - - Created detailed documentation of the retry with backoff mechanism - - Added code examples and best practices - -5. **SDK_UPDATE_INSTRUCTIONS.md** - - Added section on updating the changelog with each release - -## Technical Implementations - -### Caching System -- In-memory caching with TTL-based expiration -- Different TTLs for different data types: - - Network data: 24 hours - - Pool data: 5 minutes - - Token data: 10 minutes - - Statistics: 15 minutes - - Default: 5 minutes -- MD5 hash-based cache keys for consistent lookup -- Support for skipping cache and custom TTLs -- Cache clearing by endpoint prefix - -### Retry with Backoff -- Automatic retry for connection errors, timeouts, and server errors (5xx) -- No retry for client errors (4xx) -- Exponential backoff with configurable times -- Default backoff schedule: 100ms, 500ms, 1s, 5s -- Random jitter to prevent thundering herd problem -- Configurable maximum retry attempts - -## Testing -- Unit tests verify both caching and retry behavior -- Tests use mocking to simulate API behavior -- Coverage includes: - - Basic caching functionality - - Caching with parameters - - Cache invalidation - - Retry on connection errors - - Retry on server errors - - No retry on client errors - - Maximum retry limit - -## Documentation -- README updated with feature overview and basic examples -- Detailed documentation created in docs/ -- Examples provided for all major use cases -- Best practices included - -## Version Management -- Version bumped from 0.1.0 to 0.2.0 -- CHANGELOG created to track version history -- Update instructions enhanced to include changelog maintenance \ No newline at end of file diff --git a/dexpaprika_sdk/__init__.py b/dexpaprika_sdk/__init__.py index 9f51322..6250d5a 100644 --- a/dexpaprika_sdk/__init__.py +++ b/dexpaprika_sdk/__init__.py @@ -11,6 +11,24 @@ """ from .client import DexPaprikaClient +# Import models for easier access +from .models import ( + Network, Dex, DexesResponse, + Token, Pool, PoolsResponse, TimeIntervalMetrics, + PoolDetails, OHLCVRecord, Transaction, TransactionsResponse, + TokenSummary, TokenDetails, + DexInfo, SearchResult, + Stats +) __version__ = "0.2.0" -__all__ = ["DexPaprikaClient"] +__all__ = [ + "DexPaprikaClient", + # Models + "Network", "Dex", "DexesResponse", + "Token", "Pool", "PoolsResponse", "TimeIntervalMetrics", + "PoolDetails", "OHLCVRecord", "Transaction", "TransactionsResponse", + "TokenSummary", "TokenDetails", + "DexInfo", "SearchResult", + "Stats" +] diff --git a/dexpaprika_sdk/models/tokens.py b/dexpaprika_sdk/models/tokens.py index 1c199ee..1f107a2 100644 --- a/dexpaprika_sdk/models/tokens.py +++ b/dexpaprika_sdk/models/tokens.py @@ -9,7 +9,7 @@ class TokenSummary(BaseModel): """Summary metrics for a token.""" price_usd: float = Field(..., description="Current price in USD") - fdv: float = Field(..., description="Fully diluted valuation") + fdv: Optional[float] = Field(None, description="Fully diluted valuation (may be None for some chains like Solana)") liquidity_usd: float = Field(..., description="Total liquidity in USD") pools: Optional[int] = Field(None, description="Number of pools containing the token") @@ -34,7 +34,7 @@ class TokenDetails(BaseModel): symbol: str = Field(..., description="Token symbol") chain: str = Field(..., description="Network the token is on") decimals: int = Field(..., description="Decimal precision of the token") - total_supply: float = Field(..., description="Total supply of the token") + total_supply: Optional[float] = Field(None, description="Total supply of the token (may be None for some chains like Solana)") description: str = Field("", description="Token description") website: str = Field("", description="Token website URL") explorer: str = Field("", description="Token explorer URL") @@ -51,7 +51,7 @@ class TokenDetailsLight(BaseModel): symbol: str = Field(..., description="Token symbol") chain: str = Field(..., description="Network the token is on") decimals: int = Field(..., description="Decimal precision of the token") - total_supply: float = Field(..., description="Total supply of the token") + total_supply: Optional[float] = Field(None, description="Total supply of the token (may be None for some chains like Solana)") description: str = Field("", description="Token description") website: str = Field("", description="Token website URL") explorer: str = Field("", description="Token explorer URL") diff --git a/docs/caching_and_retry.md b/docs/caching_and_retry.md deleted file mode 100644 index cb9b3cd..0000000 --- a/docs/caching_and_retry.md +++ /dev/null @@ -1,171 +0,0 @@ -# Caching and Retry Features - -This document provides detailed information about the caching and retry features in the DexPaprika SDK. - -## Caching System - -The SDK includes an intelligent caching system that helps reduce API calls and improve performance. - -### How Caching Works - -1. By default, all GET requests are cached with expiration times based on the type of data: - - Network data: 24 hours (rarely changes) - - Pool data: 5 minutes (changes frequently) - - Token data: 10 minutes (changes moderately) - - Statistics: 15 minutes (changes moderately) - - Other data: 5 minutes (default) - -2. The cache is keyed based on both the endpoint and query parameters, ensuring that unique requests get unique cache entries. - -3. Cached results are automatically invalidated after their TTL (Time To Live) expires, ensuring you don't receive stale data. - -### Using the Caching Features - -#### Basic Caching - -Caching happens automatically - you don't need to do anything special: - -```python -from dexpaprika_sdk import DexPaprikaClient - -client = DexPaprikaClient() - -# First call hits the API (slower) -networks = client.networks.list() - -# Second call uses cached data (much faster) -networks_again = client.networks.list() -``` - -#### Skipping the Cache - -When you need fresh data, you can bypass the cache: - -```python -# Force fresh data by skipping the cache -fresh_data = client.networks._get("/networks", skip_cache=True) -``` - -#### Custom TTL - -Advanced usage allows you to set a custom TTL for specific requests: - -```python -from datetime import timedelta - -# Cache this response for only 30 seconds -short_lived_data = client.networks._get( - "/networks", - ttl=timedelta(seconds=30) -) - -# Cache this response for a day -long_lived_data = client.pools._get( - "/networks/ethereum/pools", - ttl=timedelta(days=1) -) -``` - -#### Clearing the Cache - -You can clear the entire cache or just parts of it: - -```python -# Clear all cached data -client.clear_cache() - -# Clear only network-related cached data -client.clear_cache(endpoint_prefix="/networks") -``` - -### Technical Details - -The caching system uses a dictionary-based in-memory cache with the following components: - -1. **CacheEntry Class**: Stores data and expiration time -2. **Cache Key Generation**: Creates unique MD5 hash-based keys for each request -3. **TTL Management**: Different TTLs based on data type -4. **Parameterized Caching**: Supports caching requests with different parameters - -## Retry with Backoff - -The SDK automatically retries failed API requests with exponential backoff to handle transient errors. - -### How Retry Works - -1. When an API request fails, the SDK evaluates if the error is retryable: - - Connection errors (network issues) → Retry - - Timeouts → Retry - - Server errors (HTTP 500-599) → Retry - - Client errors (HTTP 400-499) → Don't retry - -2. If the error is retryable, the SDK will: - - Wait for a specified backoff time - - Add some random jitter to prevent thundering herd problems - - Retry the request - - Repeat until success or max retries is reached - -3. Default backoff times: - - 1st retry: 100ms - - 2nd retry: 500ms - - 3rd retry: 1000ms (1 second) - - 4th retry: 5000ms (5 seconds) - -### Configuring Retry Behavior - -You can customize the retry behavior when creating the client: - -```python -from dexpaprika_sdk import DexPaprikaClient - -# Default settings -default_client = DexPaprikaClient() - -# Custom retry settings -custom_client = DexPaprikaClient( - max_retries=3, # Maximum 3 retry attempts - backoff_times=[0.2, 1.0, 3.0] # Custom backoff times in seconds -) - -# Disable retries entirely -no_retry_client = DexPaprikaClient(max_retries=0) -``` - -### When to Use Custom Retry Settings - -- **High-throughput applications**: You might want to use shorter backoff times -- **Background jobs**: You might want more retries with longer backoff times -- **User-facing applications**: Balance between responsiveness and reliability - -### Error Handling with Retries - -Even with retries, some requests may still fail. Always implement proper error handling: - -```python -try: - # This will retry automatically on retryable errors - response = client.pools.list(limit=5) -except Exception as e: - # Handle the error after all retries have failed - print(f"Request failed after retries: {e}") -``` - -## Performance Considerations - -1. **Memory Usage**: The caching system stores responses in memory. For applications processing large amounts of data, monitor memory usage. - -2. **Request Latency**: Retries can increase the total time a request takes to complete or fail. Set appropriate timeouts for your application. - -3. **API Rate Limits**: While retries and caching can help manage rate limits, they don't completely solve the problem. Be aware of your API usage. - -## Best Practices - -1. **Cache Invalidation**: Clear specific parts of the cache when you know the data has changed. - -2. **Configure Retries Appropriately**: Don't set extremely long retry sequences for user-facing applications. - -3. **Monitoring**: Track cache hit rates and retry counts to optimize your configuration. - -4. **Error Handling**: Always implement proper error handling regardless of retry mechanisms. - -5. **Testing**: Test your application's behavior when the API is slow or unavailable to ensure graceful degradation. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e10c65b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dexpaprika-sdk" +dynamic = ["version", "optional-dependencies"] # Version is read from dexpaprika_sdk/__init__.py +description = "Python SDK for the DexPaprika API" +readme = "README.md" +authors = [ + {name = "CoinPaprika", email = "support@coinpaprika.com"} +] +license = {text = "MIT"} +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", +] +keywords = ["dexpaprika", "crypto", "blockchain", "defi", "api", "sdk"] +dependencies = [ + "requests>=2.25.0", + "pydantic>=2.0.0", +] + +[project.urls] +"Homepage" = "https://github.com/coinpaprika/dexpaprika-sdk-python" +"Bug Tracker" = "https://github.com/coinpaprika/dexpaprika-sdk-python/issues" +"Documentation" = "https://docs.dexpaprika.com" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..cd0f301 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v \ No newline at end of file diff --git a/setup.py b/setup.py index beb70c5..4519381 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,17 @@ +import re from setuptools import setup, find_packages +# Read version from __init__.py +with open("dexpaprika_sdk/__init__.py", "r") as f: + version_match = re.search(r'__version__ = "(.*?)"', f.read()) + version = version_match.group(1) if version_match else "0.0.0" + setup( name="dexpaprika-sdk", - version="0.2.0", + version=version, description="Python SDK for the DexPaprika API", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", author="CoinPaprika", author_email="support@coinpaprika.com", url="https://github.com/coinpaprika/dexpaprika-sdk-python", @@ -12,6 +20,17 @@ "requests>=2.25.0", "pydantic>=2.0.0", ], + extras_require={ + "dev": [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.0.0", + "twine>=4.0.0", + "build>=0.10.0", + ], + }, python_requires=">=3.8", classifiers=[ "Development Status :: 4 - Beta", @@ -22,5 +41,10 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", ], + keywords=["dexpaprika", "crypto", "blockchain", "defi", "api", "sdk"], ) \ No newline at end of file diff --git a/test_all_endpoints.py b/test_all_endpoints.py deleted file mode 100644 index 3ad1922..0000000 --- a/test_all_endpoints.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import os -import time -from datetime import datetime, timedelta -import traceback - -# Add the parent directory to the path so we can import the package -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) - -from dexpaprika_sdk import DexPaprikaClient - -def print_separator(): - print("\n" + "-" * 50 + "\n") - -def test_endpoint(name, test_func): - print(f"Testing {name}...") - try: - result = test_func() - print(f"✅ {name} - SUCCESS") - return True, result - except Exception as e: - print(f"❌ {name} - FAILED: {str(e)}") - traceback.print_exc() - return False, None - -def main(): - # Create a new DexPaprika client - client = DexPaprikaClient() - ethereum_network = "ethereum" - success_count = 0 - failure_count = 0 - - # 1. Test Networks API - print_separator() - print("TESTING NETWORKS API") - print_separator() - - success, networks = test_endpoint("networks.list", lambda: client.networks.list()) - if success: - success_count += 1 - print(f"Found {len(networks)} networks") - else: - failure_count += 1 - - success, _ = test_endpoint("networks.list_dexes", lambda: client.networks.list_dexes(ethereum_network)) - if success: - success_count += 1 - else: - failure_count += 1 - - # 2. Test Utils API - print_separator() - print("TESTING UTILS API") - print_separator() - - success, stats = test_endpoint("utils.get_stats", lambda: client.utils.get_stats()) - if success: - success_count += 1 - print(f"- Chains: {stats.chains}") - print(f"- Factories: {stats.factories}") - print(f"- Pools: {stats.pools}") - print(f"- Tokens: {stats.tokens}") - else: - failure_count += 1 - - # 3. Test Pools API - print_separator() - print("TESTING POOLS API") - print_separator() - - success, pools_response = test_endpoint("pools.list", lambda: client.pools.list(limit=5)) - if success: - success_count += 1 - print(f"Found {len(pools_response.pools)} pools") - if pools_response.pools: - # Save the first pool for later tests - test_pool = pools_response.pools[0] - test_pool_network = test_pool.chain - test_pool_address = test_pool.id - print(f"Using pool {test_pool_address} on {test_pool.chain} for further tests") - else: - failure_count += 1 - # Use Ethereum USDC/WETH as fallback - test_pool_network = "ethereum" - test_pool_address = "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640" # USDC/WETH on Uniswap v3 - - success, _ = test_endpoint("pools.list_by_network", lambda: client.pools.list_by_network(ethereum_network, limit=5)) - if success: - success_count += 1 - else: - failure_count += 1 - - success, dexes_response = test_endpoint("dexes.list", lambda: client.dexes.list(ethereum_network)) - if success: - success_count += 1 - # Use a known popular DEX instead of the first one from the list - test_dex = "uniswap_v3" # Uniswap V3 on Ethereum - - success, _ = test_endpoint("pools.list_by_dex", lambda: client.pools.list_by_dex(ethereum_network, test_dex, limit=5)) - if success: - success_count += 1 - else: - failure_count += 1 - else: - failure_count += 1 - - success, pool_details = test_endpoint("pools.get_details", lambda: client.pools.get_details(test_pool_network, test_pool_address)) - if success: - success_count += 1 - print(f"Pool details: {pool_details.dex_name}") - if hasattr(pool_details, 'tokens') and len(pool_details.tokens) >= 2: - # Save token addresses for token API tests - test_token_address = pool_details.tokens[0].id - print(f"Using token {test_token_address} for further tests") - else: - failure_count += 1 - # Use USDC on Ethereum as fallback - test_token_address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" # USDC on Ethereum - - # Use dates for OHLCV - end_date = datetime.now() - start_date = end_date - timedelta(days=7) - start_str = start_date.strftime("%Y-%m-%d") - end_str = end_date.strftime("%Y-%m-%d") - - success, _ = test_endpoint("pools.get_ohlcv", lambda: client.pools.get_ohlcv( - test_pool_network, - test_pool_address, - start=start_str, - end=end_str, - limit=5, - interval="24h" - )) - if success: - success_count += 1 - else: - failure_count += 1 - - success, _ = test_endpoint("pools.get_transactions", lambda: client.pools.get_transactions(test_pool_network, test_pool_address, limit=5)) - if success: - success_count += 1 - else: - failure_count += 1 - - # 4. Test Tokens API - print_separator() - print("TESTING TOKENS API") - print_separator() - - success, token_details = test_endpoint("tokens.get_details", lambda: client.tokens.get_details(test_pool_network, test_token_address)) - if success: - success_count += 1 - print(f"Token details: {token_details.name} ({token_details.symbol})") - else: - failure_count += 1 - - success, _ = test_endpoint("tokens.get_pools", lambda: client.tokens.get_pools(test_pool_network, test_token_address, limit=5)) - if success: - success_count += 1 - else: - failure_count += 1 - - # 5. Test Search API - print_separator() - print("TESTING SEARCH API") - print_separator() - - success, search_results = test_endpoint("search.search", lambda: client.search.search("Jockey")) - if success: - success_count += 1 - print(f"Found {len(search_results.tokens)} tokens, {len(search_results.pools)} pools, and {len(search_results.dexes)} DEXes") - else: - failure_count += 1 - - # Print summary - print_separator() - print(f"SUMMARY: {success_count} endpoints succeeded, {failure_count} endpoints failed") - print_separator() - - if failure_count > 0: - sys.exit(1) - else: - print("All endpoints tested successfully!") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..475298b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,163 @@ +import pytest +from unittest.mock import MagicMock, patch +import sys +import os +import json +from datetime import datetime +from pathlib import Path + +# Add the parent directory to the path so we can import the package +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from dexpaprika_sdk import DexPaprikaClient +from dexpaprika_sdk.models import ( + Network, Dex, DexesResponse, + Token, Pool, PoolsResponse, TimeIntervalMetrics, + PoolDetails, OHLCVRecord, Transaction, TransactionsResponse, + TokenSummary, TokenDetails, + DexInfo, SearchResult, + Stats +) + +# Create a fixture for determining whether to use mocks or real API +@pytest.fixture +def use_mocks(): + # Set this to False to use the real API instead of mocks + return True + +# Create a fixture for the mock or real client based on the use_mocks flag +@pytest.fixture +def client(use_mocks): + if use_mocks: + return create_mock_client() + else: + return DexPaprikaClient() + +# Create a fixture for test data +@pytest.fixture +def test_data(): + return { + "ethereum_network": "ethereum", + "test_pool_network": "ethereum", + "test_pool_address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", # USDC/WETH on Uniswap v3 + "test_dex": "uniswap_v3", # Uniswap V3 on Ethereum + "test_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", # USDC on Ethereum + "start_date": (datetime.now()).strftime("%Y-%m-%d"), + "end_date": (datetime.now()).strftime("%Y-%m-%d"), + } + +def create_mock_client(): + """Creates a mocked client that returns pre-defined responses for tests""" + client = DexPaprikaClient() + + # Mock Networks API + networks = [Network(id="ethereum", display_name="Ethereum")] + client.networks.list = MagicMock(return_value=networks) + + dexes_response = DexesResponse( + dexes=[Dex(id="uniswap_v3", name="Uniswap V3", url="https://uniswap.org")] + ) + client.networks.list_dexes = MagicMock(return_value=dexes_response) + client.dexes.list = MagicMock(return_value=dexes_response) + + # Mock Utils API + stats = Stats(chains=20, factories=100, pools=5000, tokens=10000) + client.utils.get_stats = MagicMock(return_value=stats) + + # Mock Pools API + token1 = Token(id="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", name="USD Coin", symbol="USDC") + token2 = Token(id="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", name="Wrapped Ether", symbol="WETH") + + pool = Pool( + id="0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + chain="ethereum", + dex_id="uniswap_v3", + dex_name="Uniswap V3", + volume_usd=1000000.0, + tokens=[token1, token2] + ) + + pools_response = PoolsResponse( + pools=[pool], + page_info=None + ) + + client.pools.list = MagicMock(return_value=pools_response) + client.pools.list_by_network = MagicMock(return_value=pools_response) + client.pools.list_by_dex = MagicMock(return_value=pools_response) + + # Mock Pool Details + time_metrics = TimeIntervalMetrics( + volume_usd=1000000.0, + txns=500, + last_price_usd=1800.0, + last_price_usd_change=2.5 + ) + + pool_details = PoolDetails( + id="0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + chain="ethereum", + dex_id="uniswap_v3", + dex_name="Uniswap V3", + tokens=[token1, token2], + last_price_usd=1800.0, + volume_usd=1000000.0, + hour1=time_metrics, + hour24=time_metrics, + day=time_metrics + ) + + client.pools.get_details = MagicMock(return_value=pool_details) + + # Mock OHLCV data + ohlcv_record = OHLCVRecord( + time=int(datetime.now().timestamp()), + open=1700.0, + high=1850.0, + low=1650.0, + close=1800.0, + volume=500000.0 + ) + + client.pools.get_ohlcv = MagicMock(return_value=[ohlcv_record]) + + # Mock transactions + transaction = Transaction( + tx_hash="0x123456789abcdef", + block_number=12345678, + timestamp=int(datetime.now().timestamp()), + type="swap", + amount_usd=1000.0 + ) + + transactions_response = TransactionsResponse( + transactions=[transaction], + page_info=None + ) + + client.pools.get_transactions = MagicMock(return_value=transactions_response) + + # Mock Tokens API + token_details = TokenDetails( + id="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + name="USD Coin", + symbol="USDC", + decimals=6, + chain="ethereum", + price_usd=1.0, + market_cap_usd=27000000000.0 + ) + + client.tokens.get_details = MagicMock(return_value=token_details) + client.tokens.get_pools = MagicMock(return_value=pools_response) + + # Mock Search API + search_result = SearchResult( + tokens=[token_details], + pools=[pool], + dexes=[Dex(id="uniswap_v3", name="Uniswap V3", url="https://uniswap.org")] + ) + + client.search.search = MagicMock(return_value=search_result) + + return client \ No newline at end of file diff --git a/tests/test_all_endpoints.py b/tests/test_all_endpoints.py new file mode 100644 index 0000000..67dfbf5 --- /dev/null +++ b/tests/test_all_endpoints.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +import sys +import os +import time +import pytest +from datetime import datetime, timedelta + +# Add the parent directory to the path so we can import the package +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) + +from dexpaprika_sdk import DexPaprikaClient + +# Create a fixture for the client to be reused across tests +@pytest.fixture +def client(): + return DexPaprikaClient() + +# Create a fixture for test data +@pytest.fixture +def test_data(): + return { + "ethereum_network": "ethereum", + "test_pool_network": "ethereum", + "test_pool_address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", # USDC/WETH on Uniswap v3 + "test_dex": "uniswap_v3", # Uniswap V3 on Ethereum + "test_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", # USDC on Ethereum + "start_date": (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"), + "end_date": datetime.now().strftime("%Y-%m-%d"), + } + +# Networks API tests +def test_networks_list(client): + networks = client.networks.list() + assert networks is not None + assert len(networks) > 0 + +def test_networks_list_dexes(client, test_data): + dexes = client.networks.list_dexes(test_data["ethereum_network"]) + assert dexes is not None + assert hasattr(dexes, 'dexes') + +# Utils API tests +def test_utils_get_stats(client): + stats = client.utils.get_stats() + assert stats is not None + assert hasattr(stats, 'chains') + assert hasattr(stats, 'pools') + assert hasattr(stats, 'tokens') + +# Pools API tests +def test_pools_list(client): + pools_response = client.pools.list(limit=5) + assert pools_response is not None + assert hasattr(pools_response, 'pools') + assert len(pools_response.pools) > 0 + +def test_pools_list_by_network(client, test_data): + pools_response = client.pools.list_by_network(test_data["ethereum_network"], limit=5) + assert pools_response is not None + assert hasattr(pools_response, 'pools') + +def test_dexes_list(client, test_data): + dexes_response = client.dexes.list(test_data["ethereum_network"]) + assert dexes_response is not None + assert hasattr(dexes_response, 'dexes') + +def test_pools_list_by_dex(client, test_data): + pools_response = client.pools.list_by_dex( + test_data["ethereum_network"], + test_data["test_dex"], + limit=5 + ) + assert pools_response is not None + assert hasattr(pools_response, 'pools') + +def test_pools_get_details(client, test_data): + pool_details = client.pools.get_details( + test_data["test_pool_network"], + test_data["test_pool_address"] + ) + assert pool_details is not None + assert hasattr(pool_details, 'tokens') + +def test_pools_get_ohlcv(client, test_data): + ohlcv = client.pools.get_ohlcv( + test_data["test_pool_network"], + test_data["test_pool_address"], + start=test_data["start_date"], + end=test_data["end_date"], + limit=5, + interval="24h" + ) + assert ohlcv is not None + +def test_pools_get_transactions(client, test_data): + transactions = client.pools.get_transactions( + test_data["test_pool_network"], + test_data["test_pool_address"], + limit=5 + ) + assert transactions is not None + assert hasattr(transactions, 'transactions') + +# Tokens API tests +def test_tokens_get_details(client, test_data): + token_details = client.tokens.get_details( + test_data["test_pool_network"], + test_data["test_token_address"] + ) + assert token_details is not None + assert hasattr(token_details, 'name') + assert hasattr(token_details, 'symbol') + +def test_tokens_get_pools(client, test_data): + token_pools = client.tokens.get_pools( + test_data["test_pool_network"], + test_data["test_token_address"], + limit=5 + ) + assert token_pools is not None + assert hasattr(token_pools, 'pools') + +# Search API tests +def test_search_search(client): + search_results = client.search.search("Jockey") + assert search_results is not None + assert hasattr(search_results, 'tokens') + assert hasattr(search_results, 'pools') + assert hasattr(search_results, 'dexes') + +# Ensure the main function still works when script is run directly +if __name__ == "__main__": + # Create client + client = DexPaprikaClient() + test_data = { + "ethereum_network": "ethereum", + "test_pool_network": "ethereum", + "test_pool_address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + "test_dex": "uniswap_v3", + "test_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "start_date": (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d"), + "end_date": datetime.now().strftime("%Y-%m-%d"), + } + + # Run all test functions + print("Running tests manually...") + test_functions = [ + test_networks_list, + test_networks_list_dexes, + test_utils_get_stats, + test_pools_list, + test_pools_list_by_network, + test_dexes_list, + test_pools_list_by_dex, + test_pools_get_details, + test_pools_get_ohlcv, + test_pools_get_transactions, + test_tokens_get_details, + test_tokens_get_pools, + test_search_search + ] + + success_count = 0 + failure_count = 0 + + for test_func in test_functions: + print(f"Testing {test_func.__name__}...") + try: + if test_func.__name__ == "test_networks_list" or test_func.__name__ == "test_utils_get_stats" or test_func.__name__ == "test_pools_list" or test_func.__name__ == "test_search_search": + test_func(client) + else: + test_func(client, test_data) + print(f"✅ {test_func.__name__} - SUCCESS") + success_count += 1 + except Exception as e: + print(f"❌ {test_func.__name__} - FAILED: {str(e)}") + import traceback + traceback.print_exc() + failure_count += 1 + + # Print summary + print("\n" + "-" * 50 + "\n") + print(f"SUMMARY: {success_count} endpoints succeeded, {failure_count} endpoints failed") + print("\n" + "-" * 50 + "\n") + + if failure_count > 0: + sys.exit(1) + else: + print("All endpoints tested successfully!") \ No newline at end of file diff --git a/test_features.py b/tests/test_features.py similarity index 100% rename from test_features.py rename to tests/test_features.py diff --git a/test_validation.py b/tests/test_validation.py similarity index 100% rename from test_validation.py rename to tests/test_validation.py