Chapter 2: Setting Up Your Development Environment

Overview

A properly configured development environment eliminates friction and reduces errors. This chapter guides you through setting up everything you need to build Etsy API applications in Python.

Creating an Etsy Developer Account

Before accessing the API, you need to register as an Etsy developer:

Step 1: Access the Developer Portal

Navigate to https://www.etsy.com/developers and sign in with your Etsy account.

Step 2: Create a New Application

  1. Click “Create a New App”
  2. Fill in the application details:
    • App Name: A descriptive name (e.g., “My Digital Shop Manager”)
    • Description: Brief explanation of your app’s purpose
    • App Website: Your website or GitHub repository URL

Step 3: Configure OAuth Settings

For OAuth 2.0 authentication, you need to specify:

Step 4: Note Your Credentials

After creation, you’ll receive:

Important: Never commit these credentials to version control. We’ll configure secure storage shortly.

Python Environment Setup

Installing Python

Ensure you have Python 3.8 or later:

# Check your Python version
python3 --version

# On Ubuntu/Debian
sudo apt update
sudo apt install python3 python3-pip python3-venv

# On macOS with Homebrew
brew install python3

# On Windows, download from python.org

Creating a Virtual Environment

Always use virtual environments to isolate project dependencies:

# Create project directory
mkdir etsy-api-project
cd etsy-api-project

# Create virtual environment
python3 -m venv venv

# Activate the environment
# On Linux/macOS:
source venv/bin/activate

# On Windows:
.\venv\Scripts\activate

# Verify activation (should show venv path)
which python

Installing Required Packages

Create a requirements.txt file with essential dependencies:

# HTTP requests
requests>=2.28.0
httpx>=0.24.0

# Environment and configuration
python-dotenv>=1.0.0

# Data handling
pandas>=2.0.0

# Authentication
authlib>=1.2.0

# Async support
aiohttp>=3.8.0

# Development tools
pytest>=7.0.0
black>=23.0.0
flake8>=6.0.0

# Optional: for building dashboards
plotly>=5.0.0
dash>=2.0.0

Install the dependencies:

pip install -r requirements.txt

Secure Credential Management

Never hardcode API credentials. Use environment variables:

Create a .env File

# .env file (add to .gitignore!)
ETSY_API_KEY=your_api_key_here
ETSY_SHARED_SECRET=your_shared_secret_here
ETSY_SHOP_ID=your_shop_id
ETSY_ACCESS_TOKEN=your_access_token
ETSY_REFRESH_TOKEN=your_refresh_token

Create a .gitignore File

# .gitignore
.env
*.pyc
__pycache__/
venv/
.vscode/
*.log
tokens.json

Loading Environment Variables

# config.py
import os
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

class Config:
    """Application configuration from environment variables."""
    
    ETSY_API_KEY = os.getenv('ETSY_API_KEY')
    ETSY_SHARED_SECRET = os.getenv('ETSY_SHARED_SECRET')
    ETSY_SHOP_ID = os.getenv('ETSY_SHOP_ID')
    ETSY_ACCESS_TOKEN = os.getenv('ETSY_ACCESS_TOKEN')
    ETSY_REFRESH_TOKEN = os.getenv('ETSY_REFRESH_TOKEN')
    
    # API settings
    BASE_URL = "https://openapi.etsy.com/v3"
    OAUTH_URL = "https://www.etsy.com/oauth"
    
    @classmethod
    def validate(cls):
        """Ensure required configuration is present."""
        required = ['ETSY_API_KEY']
        missing = [key for key in required if not getattr(cls, key)]
        
        if missing:
            raise ValueError(f"Missing required configuration: {', '.join(missing)}")
        
        return True

# Validate on import
if __name__ == "__main__":
    Config.validate()
    print("Configuration loaded successfully!")

Project Structure

Organize your project for maintainability:

etsy-api-project/
├── .env                    # Environment variables (not in git)
├── .gitignore
├── requirements.txt
├── config.py               # Configuration management
├── etsy_client/
│   ├── __init__.py
│   ├── client.py           # Main API client
│   ├── auth.py             # Authentication handling
│   ├── listings.py         # Listing operations
│   ├── orders.py           # Order operations
│   ├── files.py            # Digital file operations
│   └── exceptions.py       # Custom exceptions
├── scripts/
│   ├── authorize.py        # OAuth authorization script
│   ├── sync_listings.py    # Listing sync utilities
│   └── export_analytics.py # Analytics export
├── tests/
│   ├── __init__.py
│   ├── test_client.py
│   └── test_listings.py
└── data/
    └── .gitkeep            # For storing exported data

Initialize the Package Structure

# Create directories
mkdir -p etsy_client scripts tests data

# Create __init__.py files
touch etsy_client/__init__.py tests/__init__.py

# Create placeholder for data directory
touch data/.gitkeep

Base API Client Implementation

Let’s create the foundation for all API interactions:

# etsy_client/exceptions.py
"""Custom exceptions for Etsy API client."""

class EtsyAPIError(Exception):
    """Base exception for Etsy API errors."""
    
    def __init__(self, message, status_code=None, response=None):
        self.message = message
        self.status_code = status_code
        self.response = response
        super().__init__(self.message)


class AuthenticationError(EtsyAPIError):
    """Raised when authentication fails."""
    pass


class RateLimitError(EtsyAPIError):
    """Raised when rate limit is exceeded."""
    
    def __init__(self, message, retry_after=60, **kwargs):
        self.retry_after = retry_after
        super().__init__(message, **kwargs)


class NotFoundError(EtsyAPIError):
    """Raised when a resource is not found."""
    pass


class ValidationError(EtsyAPIError):
    """Raised when request validation fails."""
    pass
# etsy_client/client.py
"""Core Etsy API client implementation."""

import time
import logging
from typing import Optional, Dict, Any
import requests

from .exceptions import (
    EtsyAPIError,
    AuthenticationError,
    RateLimitError,
    NotFoundError,
    ValidationError
)

logger = logging.getLogger(__name__)


class EtsyClient:
    """
    Main client for interacting with the Etsy API v3.
    
    Usage:
        client = EtsyClient(api_key="your_key", access_token="your_token")
        shop = client.get_shop(shop_id)
    """
    
    BASE_URL = "https://openapi.etsy.com/v3"
    
    def __init__(
        self,
        api_key: str,
        access_token: Optional[str] = None,
        shop_id: Optional[str] = None,
        timeout: int = 30,
        max_retries: int = 3,
        dry_run: bool = False
    ):
        """
        Initialize the Etsy API client.
        
        Args:
            api_key: Your Etsy API key (keystring)
            access_token: OAuth access token for authenticated requests
            shop_id: Default shop ID for operations
            timeout: Request timeout in seconds
            max_retries: Maximum retry attempts for failed requests
            dry_run: If True, log requests without executing writes
        """
        self.api_key = api_key
        self.access_token = access_token
        self.shop_id = shop_id
        self.timeout = timeout
        self.max_retries = max_retries
        self.dry_run = dry_run
        
        self._session = requests.Session()
        self._setup_session()
    
    def _setup_session(self):
        """Configure the requests session with default headers."""
        self._session.headers.update({
            "x-api-key": self.api_key,
            "Content-Type": "application/json",
            "Accept": "application/json"
        })
        
        if self.access_token:
            self._session.headers["Authorization"] = f"Bearer {self.access_token}"
    
    def set_access_token(self, access_token: str):
        """Update the access token for authenticated requests."""
        self.access_token = access_token
        self._session.headers["Authorization"] = f"Bearer {access_token}"
    
    def _build_url(self, endpoint: str) -> str:
        """Build the full URL for an API endpoint."""
        return f"{self.BASE_URL}{endpoint}"
    
    def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
        """
        Handle API response and raise appropriate exceptions.
        
        Args:
            response: The requests Response object
            
        Returns:
            Parsed JSON response data
            
        Raises:
            EtsyAPIError: For various API errors
        """
        # Log rate limit info
        remaining = response.headers.get('X-RateLimit-Remaining')
        if remaining:
            logger.debug(f"Rate limit remaining: {remaining}")
        
        # Handle different status codes
        if response.status_code == 200:
            return response.json() if response.content else {}
        
        elif response.status_code == 201:
            return response.json() if response.content else {"created": True}
        
        elif response.status_code == 204:
            return {"deleted": True}
        
        elif response.status_code == 401:
            raise AuthenticationError(
                "Authentication failed. Check your API key and access token.",
                status_code=401,
                response=response
            )
        
        elif response.status_code == 403:
            raise AuthenticationError(
                "Access forbidden. Check your OAuth scopes.",
                status_code=403,
                response=response
            )
        
        elif response.status_code == 404:
            raise NotFoundError(
                f"Resource not found: {response.url}",
                status_code=404,
                response=response
            )
        
        elif response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            raise RateLimitError(
                f"Rate limit exceeded. Retry after {retry_after} seconds.",
                retry_after=retry_after,
                status_code=429,
                response=response
            )
        
        elif response.status_code == 400:
            error_data = response.json() if response.content else {}
            raise ValidationError(
                f"Validation error: {error_data.get('error', 'Unknown error')}",
                status_code=400,
                response=response
            )
        
        else:
            error_data = response.json() if response.content else {}
            raise EtsyAPIError(
                f"API error: {error_data.get('error', 'Unknown error')}",
                status_code=response.status_code,
                response=response
            )
    
    def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict] = None,
        json: Optional[Dict] = None,
        files: Optional[Dict] = None,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Make an API request with retry logic.
        
        Args:
            method: HTTP method (GET, POST, PUT, DELETE)
            endpoint: API endpoint path
            params: Query parameters
            json: JSON body data
            files: Files to upload
            **kwargs: Additional requests arguments
            
        Returns:
            Parsed JSON response
        """
        url = self._build_url(endpoint)
        
        # Dry run for write operations
        if self.dry_run and method in ('POST', 'PUT', 'PATCH', 'DELETE'):
            logger.info(f"[DRY RUN] {method} {url}")
            logger.info(f"[DRY RUN] Params: {params}")
            logger.info(f"[DRY RUN] Body: {json}")
            return {"dry_run": True, "method": method, "url": url}
        
        last_exception = None
        
        for attempt in range(self.max_retries):
            try:
                logger.debug(f"{method} {url}")
                
                # Handle file uploads differently
                headers = None
                if files:
                    headers = {k: v for k, v in self._session.headers.items() 
                              if k != 'Content-Type'}
                
                response = self._session.request(
                    method=method,
                    url=url,
                    params=params,
                    json=json,
                    files=files,
                    headers=headers,
                    timeout=self.timeout,
                    **kwargs
                )
                
                return self._handle_response(response)
            
            except RateLimitError as e:
                logger.warning(f"Rate limited, waiting {e.retry_after}s (attempt {attempt + 1})")
                time.sleep(e.retry_after)
                last_exception = e
            
            except requests.exceptions.Timeout:
                logger.warning(f"Request timeout (attempt {attempt + 1})")
                time.sleep(2 ** attempt)  # Exponential backoff
                last_exception = EtsyAPIError("Request timeout")
            
            except requests.exceptions.ConnectionError:
                logger.warning(f"Connection error (attempt {attempt + 1})")
                time.sleep(2 ** attempt)
                last_exception = EtsyAPIError("Connection error")
        
        raise last_exception or EtsyAPIError("Max retries exceeded")
    
    # Convenience methods
    def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make a GET request."""
        return self._request('GET', endpoint, **kwargs)
    
    def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make a POST request."""
        return self._request('POST', endpoint, **kwargs)
    
    def put(self, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make a PUT request."""
        return self._request('PUT', endpoint, **kwargs)
    
    def patch(self, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make a PATCH request."""
        return self._request('PATCH', endpoint, **kwargs)
    
    def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make a DELETE request."""
        return self._request('DELETE', endpoint, **kwargs)
    
    # Basic resource methods
    def get_me(self) -> Dict[str, Any]:
        """Get the currently authenticated user."""
        return self.get("/application/users/me")
    
    def get_shop(self, shop_id: Optional[str] = None) -> Dict[str, Any]:
        """Get shop information."""
        shop_id = shop_id or self.shop_id
        if not shop_id:
            raise ValueError("shop_id is required")
        return self.get(f"/application/shops/{shop_id}")
    
    def ping(self) -> bool:
        """Test API connectivity."""
        try:
            self.get("/application/openapi-ping")
            return True
        except EtsyAPIError:
            return False
# etsy_client/__init__.py
"""Etsy API Client package."""

from .client import EtsyClient
from .exceptions import (
    EtsyAPIError,
    AuthenticationError,
    RateLimitError,
    NotFoundError,
    ValidationError
)

__all__ = [
    'EtsyClient',
    'EtsyAPIError',
    'AuthenticationError',
    'RateLimitError',
    'NotFoundError',
    'ValidationError'
]

__version__ = '1.0.0'

Testing Your Setup

Create a simple test script:

# scripts/test_setup.py
"""Test script to verify the development environment setup."""

import sys
from pathlib import Path

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from config import Config
from etsy_client import EtsyClient


def test_configuration():
    """Test that configuration loads correctly."""
    print("Testing configuration...")
    
    try:
        Config.validate()
        print("✓ Configuration loaded successfully")
        print(f"  API Key: {Config.ETSY_API_KEY[:8]}...")
        return True
    except ValueError as e:
        print(f"✗ Configuration error: {e}")
        return False


def test_api_connection():
    """Test basic API connectivity."""
    print("\nTesting API connection...")
    
    client = EtsyClient(
        api_key=Config.ETSY_API_KEY,
        access_token=Config.ETSY_ACCESS_TOKEN
    )
    
    if client.ping():
        print("✓ API connection successful")
        return True
    else:
        print("✗ API connection failed")
        return False


def test_authenticated_request():
    """Test authenticated API access."""
    print("\nTesting authenticated access...")
    
    if not Config.ETSY_ACCESS_TOKEN:
        print("⚠ No access token configured, skipping")
        return True
    
    client = EtsyClient(
        api_key=Config.ETSY_API_KEY,
        access_token=Config.ETSY_ACCESS_TOKEN
    )
    
    try:
        user = client.get_me()
        print(f"✓ Authenticated as user: {user.get('user_id')}")
        return True
    except Exception as e:
        print(f"✗ Authentication failed: {e}")
        return False


def main():
    """Run all setup tests."""
    print("=" * 50)
    print("Etsy API Development Environment Test")
    print("=" * 50)
    
    results = [
        test_configuration(),
        test_api_connection(),
        test_authenticated_request()
    ]
    
    print("\n" + "=" * 50)
    if all(results):
        print("All tests passed! Environment is ready.")
    else:
        print("Some tests failed. Please check the errors above.")
    print("=" * 50)


if __name__ == "__main__":
    main()

Run the test:

python scripts/test_setup.py

IDE Configuration

VS Code Settings

Create .vscode/settings.json:

{
    "python.defaultInterpreterPath": "${workspaceFolder}/venv/bin/python",
    "python.linting.enabled": true,
    "python.linting.flake8Enabled": true,
    "python.formatting.provider": "black",
    "editor.formatOnSave": true,
    "python.testing.pytestEnabled": true,
    "python.testing.pytestArgs": ["tests"],
    "python.envFile": "${workspaceFolder}/.env"
}

Launch Configuration

Create .vscode/launch.json for debugging:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "envFile": "${workspaceFolder}/.env"
        },
        {
            "name": "Python: Test Setup",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/scripts/test_setup.py",
            "console": "integratedTerminal",
            "envFile": "${workspaceFolder}/.env"
        }
    ]
}

Logging Configuration

Set up proper logging for debugging:

# logging_config.py
import logging
import sys
from pathlib import Path


def setup_logging(
    level: str = "INFO",
    log_file: str = None,
    format_string: str = None
):
    """
    Configure logging for the application.
    
    Args:
        level: Logging level (DEBUG, INFO, WARNING, ERROR)
        log_file: Optional file path for log output
        format_string: Custom log format
    """
    if format_string is None:
        format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    
    # Create handlers
    handlers = [logging.StreamHandler(sys.stdout)]
    
    if log_file:
        log_path = Path(log_file)
        log_path.parent.mkdir(parents=True, exist_ok=True)
        handlers.append(logging.FileHandler(log_file))
    
    # Configure root logger
    logging.basicConfig(
        level=getattr(logging, level.upper()),
        format=format_string,
        handlers=handlers
    )
    
    # Reduce noise from third-party libraries
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("requests").setLevel(logging.WARNING)


# Usage in scripts:
# from logging_config import setup_logging
# setup_logging(level="DEBUG", log_file="logs/etsy_api.log")

Environment Checklist

Before proceeding to authentication, verify:

Key Takeaways

  1. Virtual environments isolate dependencies: Always use one for each project
  2. Never commit credentials: Use .env files and .gitignore
  3. Modular structure: Organize code for maintainability
  4. Test early: Verify setup before building complex features
  5. Configure your IDE: Proper settings improve productivity

Moving Forward

With your development environment configured, you’re ready to implement OAuth authentication. The next chapter walks through the complete OAuth 2.0 flow with PKCE, generating access tokens that unlock the full power of the Etsy API.


← Chapter 1: Understanding the Etsy API Ecosystem Table of Contents Chapter 3: Authentication and Authorization →