# SunSolve P90 Analysis Client

A Python client library for connecting to the SunSolve P90 analysis service via gRPC. This client enables photovoltaic system P90 analysis with comprehensive error handling and type safety.

For more details please consult our [documentation](https://docs.sunsolve.com/en/p90/)

## Features

- **gRPC Client**: Direct connection to SunSolve P90 analysis service
- **Multi-Year Analysis**: Support for both TMY (Typical Mean Year) and actual multi-year datasets
- **Comprehensive Error Handling**: Custom exception classes for different error scenarios
- **Type Safety**: Full type annotations and MyPy compatibility
- **Protocol Buffer Support**: Automatic generation and management of protobuf files
- **Development Tools**: Integrated linting, formatting, and code quality checks
- **Example Implementation**: Complete working example with weather data processing

## Quick Start

Instructions here apply to users who wish to import the sunsolve_p90_client library within their own solution.
Installation of the package would not be required when using this code base directly.

### Prerequisites
- Python 3.12+ installed
- SunSolve account with **Uncertainty Analysis subscription** ([Getting started with P90](https://docs.sunsolve.com/en/p90/getting-started/))
- API access permissions enabled on your account
- Basic familiarity with Python virtual environments

### Install the Package

> **📦 Package Availability**: The wheel package is automatically created by the build pipeline and available on [pypi](https://pypi.org/project/sunsolve-p90-client/). For local development, you can build the package yourself using the instructions in the [Building the Package](#building-the-package) section.

```bash
# Install the wheel package
pip install sunsolve-p90-client
```

### Basic Usage Example

The P90 analysis service processes weather data and applies uncertainty distributions to calculate P-values over multiple simulations. This example shows the minimal setup required to perform an analysis.

```python
from pvl_p90_client.client.p90_client import P90Client
from pvl_p90_client.helpers import pvl_login
from pvl_p90_client.helpers.request_helpers import (
    build_request,
    build_distribution,
    build_gaussian_distribution,
    load_weather_data_from_pvw_file,
)
from pvl_p90_client.grpcclient.uncertaintyMessages_pb2 import DistributionInput

# Connect to the P90 analysis service
with P90Client() as client:
    # Authenticate with your SunSolve credentials (prompts for username/password)
    credentials = pvl_login.login()
    
    # Load weather data from a PVW file (Typical Mean Year format)
    weather_data = load_weather_data_from_pvw_file("data/sydney.pvw")
    
    # Add uncertainty distribution for Global Horizontal Irradiance (GHI)
    # This adds ±5% variation to solar irradiance values
    distributions = [
        build_distribution(
            input=DistributionInput.GHI,
            sim_to_sim_distribution=build_gaussian_distribution(1.0, 0.05)
        )
    ]
    
    # Create a minimal request with weather data and basic uncertainty
    # This uses default system parameters and simulation settings
    request = build_request(
        time_step_data=weather_data,
        distributions=distributions
    )
    
    # Send the analysis request and receive results
    summary, used_inputs = client.send_request(request, credentials, timeout=60.0)
    
    # Display results
    if summary:
        print(f"Analysis complete! Generated {len(summary.YearlyPValue)} yearly P-values")
        for pvalue in summary.YearlyPValue[:3]:  # Show first 3 years
            print(f"  Year {pvalue.Year}: P{pvalue.P} = {pvalue.P50Deviation:.4f}")
    else:
        print("No analysis results received")
```

**What this example does:**

- **Connects** to the P90 service using the client context manager for automatic cleanup
- **Authenticates** using your SunSolve account credentials
- **Loads weather data** from a PVW file (Typical Mean Year format)  
- **Creates a minimal request** using only weather data and basic simulation settings
- **Sends the analysis** and receives both summary results and details of inputs used
- **Displays P-values** showing the probability of exceedance for different confidence levels

> **🔧 Customization Available**: This minimal example uses default system parameters. For detailed system configuration including module specifications, electrical settings, and uncertainty distributions, see the [Request Building Guide](#request-building-guide) section below.

> **📖 For a complete working example** with detailed request building, error handling, and result processing, see [`src/example.py`](src/example.py).

---

## API Documentation

### P90Client

The main client class for interacting with the SunSolve uncertainty service.
In most cases defaults do not need to be changed.
In rare cases, the version may need to be set specifically if the version of the library is no longer
compatible with the latest server version and if an updated version of the library is not yet available.

```python
from pvl_p90_client.client.p90_client import P90Client

# Initialize client
client = P90Client(
    api_base_uri="middleware.pvlighthouse.com.au",  # Default
    version="latest-prod"                         # Default
)

# Use as context manager (recommended)
with P90Client() as client:
    # Client automatically handles connection cleanup
    summary, used_inputs = client.send_request(request, credentials, timeout=30.0)
```

**Methods**:

- `send_request(request, call_credentials, timeout=30.0)`: Send analysis request, returns `(summary, used_inputs)` tuple
- `close()`: Manually close connection
- Context manager support for automatic cleanup

**Custom Exceptions**:

- `P90ClientError`: Base exception class
- `AuthenticationError`: Authentication failures and permission denied errors
- `P90ConnectionError`: Connection issues  
- `ServiceError`: Service-level errors

> **Note**: `AuthenticationError` is raised for both invalid credentials and insufficient permissions (e.g., no Uncertainty Analysis subscription).

### Helper Functions

#### Authentication (`pvl_p90_client.helpers.pvl_login`)

```python
from pvl_p90_client.helpers import pvl_login

# Login with credentials (prompts for username/password if not provided)
credentials = pvl_login.login()

# Or provide credentials programmatically
credentials = pvl_login.login(username="your_username", password="your_password")
```

**Features:**

- Interactive credential prompting when parameters not provided
- Secure password input (hidden from console)
- Automatic token validation and error handling
- Raises `AuthenticationError` on authentication failure

#### Weather Data Loading (`pvl_p90_client.helpers.request_helpers`)

```python
from pvl_p90_client.helpers.request_helpers import (
    load_weather_data_from_pvw_file,
    load_time_step_data_from_csv
)

# Load weather data from PVW file (binary format)
weather_data = load_weather_data_from_pvw_file("data/sydney.pvw")

# Load weather data from CSV file (text format)  
weather_data = load_time_step_data_from_csv("data/sydney.csv")
```

**What these functions do:**

- **PVW files**: Load binary weather data in SunSolve's native format. This format does not include time step based tilt angle which would need to be added manually
- **CSV files**: Load text-based time step data with automatic validation and conversion
- Both return lists of `TimeStepData` protobuf messages ready for analysis
- Raise `TimeStepDataError` on file format or validation errors

## Request Building Guide

The `build_request` helper function provides a streamlined way to create analysis requests with optional detailed configuration. This section shows how to customize your analysis beyond the basic example.

### Using the build_request Helper

**Minimal usage** (uses defaults for all system parameters):
```python
from pvl_p90_client.helpers.request_helpers import (
    build_request, build_distribution, build_gaussian_distribution
)
from pvl_p90_client.grpcclient.uncertaintyMessages_pb2 import DistributionInput

# Add basic uncertainty to solar irradiance
distributions = [
    build_distribution(
        input=DistributionInput.GHI,
        sim_to_sim_distribution=build_gaussian_distribution(1.0, 0.05)  # ±5% GHI uncertainty
    )
]

# Create request with weather data and basic uncertainty - all system parameters use defaults
request = build_request(
    time_step_data=weather_data,
    distributions=distributions
)
```

**Full customization** with detailed system configuration:
```python
from pvl_p90_client.helpers.request_helpers import (
    build_request, build_module_info, build_system_info, 
    build_electrical_settings, build_optical_settings, 
    build_thermal_settings, build_operational_settings,
    build_simulation_options, build_result_options,
    build_distribution, build_gaussian_distribution,
    build_weibull_distribution, build_skewed_gaussian_distribution
)
from pvl_p90_client.grpcclient.uncertaintyMessages_pb2 import TrackingType 

# Define module physical properties
module = build_module_info(
    length=2.0,                                    # Module length in meters
    width=1.0,                                     # Module width in meters  
    cell_to_cell_mismatch=0.02,                   # Cell-to-cell variation
    power_temperature_coefficient=-0.004,         # Temperature coefficient
    power_at_stc=460,                             # Rated power at STC (Watts)
    bifaciality=0.75                              # Bifaciality factor (rear/front efficiency ratio)
)

# Configure system layout and basic parameters  
system = build_system_info(
    number_of_inverters=1,                        # Number of inverters
    strings_per_inverter=10,                      # Strings per inverter
    modules_per_string=20,                        # Modules per string
    row_pitch=5.6,                               # Distance between rows
    module_azimuth=180.0,                        # System azimuth (180° = north)
    module_tilt=25.0,                            # Default tilt when not specified per timestep
    module_tracking=TrackingType.SAT,            # Tracking type (NotSet or SAT)
    module_tilt_limit=60.0,                      # Maximum tilt angle for tracking systems
    module_height=1.0                            # Module height above ground
)

# Define electrical characteristics and losses
electrical = build_electrical_settings(
    inverter_efficiency=0.98,                    # Inverter efficiency
    module_to_module_mismatch=0.02,              # Module manufacturing variation
    string_wiring_loss=0.01,                    # DC wiring losses
    max_power_tracking_loss=0.995,              # MPPT tracking efficiency
    inverter_wiring_loss=0.01,                  # AC wiring losses
    string_to_string_mismatch=0.02,             # String-to-string variation
    inverter_to_inverter_mismatch=0.01          # Inverter-to-inverter variation
)

# Configure optical settings for irradiance processing
optical = build_optical_settings(
    beam_multiplier_front=1.0,                   # Beam irradiance adjustment (front)
    beam_multiplier_rear=1.0,                    # Beam irradiance adjustment (rear)
    isotropic_multiplier_front=1.0,              # Diffuse irradiance adjustment (front)
    isotropic_multiplier_rear=1.0,               # Diffuse irradiance adjustment (rear)
    albedo=0.2,                                  # Ground reflectance default
    soiling_front=0.0,                          # Soiling loss default (front)
    soiling_rear=0.0,                           # Soiling loss default (rear)
    spectral_correction=1.0,                    # Spectral correction default
    rear_transmission_factor=0.8,               # Rear surface transmission factor default
    rear_structural_shading_factor=0.9          # Rear structural shading factor default
)

# Set thermal modeling parameters
thermal = build_thermal_settings(
    uc=29.0,                                     # Heat loss coefficient (W/m²K)
    uv=0.0,                                      # Wind speed coefficient
    alpha=0.9                                    # Solar absorptance
)

# Configure operational parameters
operational = build_operational_settings(
    annual_degradation_rate=0.005,               # Annual power degradation (0.5%/year)
    curtailment=0.0,                             # Grid curtailment losses
    availability=0.98,                           # System availability
    dc_health=1.0,                               # DC system health factor
    undulating_ground=False,                     # Terrain type
    yield_modifier=1.0                           # Overall yield adjustment
)

# Define comprehensive uncertainty distributions
distributions = [
    # GHI uncertainty with simulation-to-simulation and year-to-year variations
    build_distribution(
        input=DistributionInput.GHI,
        sim_to_sim_distribution=build_gaussian_distribution(1.0, 0.05),      # ±5% per simulation
        yr_to_yr_distribution=build_gaussian_distribution(1.0, 0.02)         # ±2% per year
    ),
    
    # Soiling uncertainty with all three distribution types
    build_distribution(
        input=DistributionInput.SoilingFront,
        sim_to_sim_distribution=build_gaussian_distribution(0.98, 0.02),     # 2% baseline soiling ±2%
        yr_to_yr_distribution=build_weibull_distribution(1.0, 0.05, 2.0),    # Weibull year variation  
        step_to_step_distribution=build_gaussian_distribution(1.0, 0.01)     # 1% timestep noise
    ),
    
    # Module degradation with skewed distribution
    build_distribution(
        input=DistributionInput.AnnualDegradationRate,
        sim_to_sim_distribution=build_skewed_gaussian_distribution(          # Skewed degradation
            alpha=2.0,      # Skewness parameter
            zeta=0.005,     # Location (0.5%/year baseline)
            omega=0.002     # Scale parameter
        )
    )
    # Add more distributions for other inputs: Temperature, WindSpeed, Availability, etc.
]

# Set analysis parameters
simulation_options = build_simulation_options(number_of_years=25, number_of_simulations=10000)
result_options = build_result_options(bin_min=0.5, bin_delta=0.01, p_values=[5, 10, 50, 90, 95])

# Build complete request with all customizations
request = build_request(
    time_step_data=weather_data,
    module=module,
    system=system, 
    electrical=electrical,
    optical=optical,
    thermal=thermal,
    operational=operational,
    distributions=distributions,
    simulation_options=simulation_options,
    result_options=result_options
)
```

**What each configuration does:**

- **Module settings** define the physical properties and electrical characteristics of individual PV modules
- **System settings** specify the overall layout, orientation, and basic system architecture  
- **Electrical settings** configure efficiency parameters and loss factors throughout the DC and AC systems
- **Optical settings** control how irradiance data is processed and applied to front/rear surfaces
- **Thermal settings** define the heat transfer model used for temperature-dependent performance
- **Operational settings** include long-term factors like degradation, availability, and maintenance
- **Distributions** apply uncertainty variations to specific input parameters across simulations
- **Simulation options** control the number of years and Monte Carlo simulations to run
- **Result options** specify which percentile values to calculate and report

### Legacy Request Building

If you need more control or prefer the traditional approach, you can build requests manually:

```python
from pvl_p90_client.grpcclient.uncertaintyMessages_pb2 import UncertaintyRequest

# Build system components individually
request = UncertaintyRequest()
request.Module.CopyFrom(build_module_info())
request.System.CopyFrom(build_system_info()) 
request.SimulationOptions.CopyFrom(build_simulation_options(5, 1000))
request.ResultOptions.CopyFrom(build_result_options(0.75, 0.01, [5, 10, 50, 90, 95]))

# Add distributions and weather data
request.Distributions.extend(distributions)
request.TMYDataSet.TimeStepDataPoints.extend(weather_data)
```

### Understanding Uncertainty Distributions

Distributions are the core mechanism for introducing uncertainty into P90 analysis. They apply multiplicative factors to targeted input parameters across different time scales.

#### How Distribution Factors Are Applied

For each targeted input parameter, the final factor is calculated as:

```
factor = baseline_value × sim_to_sim_factor × yr_to_yr_factor × step_to_step_factor
```

**Distribution Types by Time Scale:**

- **Sim-to-sim**: Same factor applied across all years in a single simulation (systematic uncertainty)
- **Year-to-year**: Same factor applied across all timesteps in a specific year (annual variation)  
- **Step-to-step**: Different factor for each individual timestep (short-term variability)

#### Available Distribution Inputs

You can target any of these input parameters with uncertainty distributions:

**Solar Resource:**

- `GHI` - Global Horizontal Irradiance
- `DiffuseFraction` - Fraction of diffuse vs direct radiation
- `CircumsolarFraction` - Circumsolar radiation component
- `WindSpeed` - Wind speed affecting module temperature
- `Temperature` - Ambient temperature
- `Albedo` - Ground reflectance

**Module Performance:**

- `ModulePower` - Module power rating uncertainty
- `SpectralCorrection` - Spectral response corrections
- `ModulePowerTemperatureCoefficient` - Temperature coefficient variation
- `CellToCellMismatch` - Individual cell performance variation

**System Losses:**

- `SoilingFront` / `SoilingRear` - Soiling losses on module surfaces
- `InverterEfficiency` - Inverter conversion efficiency
- `InverterToInverterMismatch` - Inverter-to-inverter variations
- `StringToStringMismatch` - String-to-string performance differences
- `ModuleToModuleMismatch` - Module manufacturing tolerances

**Environmental & Operational:**

- `Uc` / `Uv` / `Alpha` - Thermal model parameters
- `AnnualDegradationRate` - Long-term power degradation
- `Availability` - System uptime and maintenance losses
- `Curtailment` - Grid curtailment requirements
- `DCHealth` - DC system health factor
- `YieldModifier` - Overall system yield adjustment
- `UndulatingGround` - Terrain effects on irradiance
- `ExtraIrradiance` - Additional irradiance adjustments
- `RearTransmissionFactor` - Bifacial rear surface transmission factor
- `RearStructuralShadingFactor` - Rear surface structural shading

**Example Distribution Targeting Multiple Inputs:**
```python
distributions = [
    # Solar resource uncertainty
    build_distribution(input=DistributionInput.GHI, sim_to_sim_distribution=build_gaussian_distribution(1.0, 0.05)),
    build_distribution(input=DistributionInput.Temperature, yr_to_yr_distribution=build_gaussian_distribution(1.0, 0.03)),
    
    # System performance uncertainty  
    build_distribution(input=DistributionInput.ModuleToModuleMismatch, sim_to_sim_distribution=build_weibull_distribution(1.0, 0.02, 2.0)),
    build_distribution(input=DistributionInput.SoilingFront, step_to_step_distribution=build_gaussian_distribution(0.98, 0.01)),
    
    # Long-term operational uncertainty
    build_distribution(input=DistributionInput.AnnualDegradationRate, sim_to_sim_distribution=build_skewed_gaussian_distribution(2.0, 0.005, 0.002))
]
```

---

## Example Usage

The `src/example.py` file demonstrates a complete p90 analysis workflow:

### Running the Example

**Using VS Code Launch Configuration** (recommended):
```bash
# Method 1: Using Run and Debug view
# - Open Run and Debug view (Ctrl+Shift+D)
# - Select "Run Example Script" from dropdown
# - Click the green play button

# Method 2: Using Command Palette
# - Press Ctrl+Shift+P
# - Type "Debug: Start Debugging"
# - Select "Run Example Script"

# Method 3: Using keyboard shortcut
# - Press F5 (if "Run Example Script" is active configuration)
```

**Manual execution**:
```bash
cd src
python example.py
```

### Example Workflow

The example demonstrates:

1. **Authentication**: Login to SunSolve service
2. **Data Loading**: Load weather data from included Sydney dataset
3. **Request Building**: Create uncertainty distributions and analysis parameters
4. **Analysis Execution**: Send request and process streaming results
5. **Error Handling**: Comprehensive error handling for all failure scenarios

```python
def main():
    with P90Client() as client:
        # Step 1: Authenticate
        if not (call_credentials := pvl_login.login()):
            print("❌ Authentication failed")
            return
            
        # Step 2: Load weather data
        if not (weather_data := load_weather_data_from_pvw_file("../data/sydney.pvw")):
            print("❌ Failed to load weather data")
            return
            
        # Step 3: Build request with distributions
        distributions = create_distributions()  # GHI, soiling, availability distributions
        request = build_request(time_step_data=weather_data, distributions=distributions)
        
        # Step 4: Execute analysis
        perform_analysis(client, request, call_credentials)
```

### Expected Output

```
Connecting to SunSolve...
Authenticating...
Enter username: your_username
Password: ********
Authentication successful!
Loading weather data...
Loaded 8760 weather data points
Creating uncertainty distributions...
Built request with 3 distributions
Starting P90 analysis...
Progress: 5.0%
...
🎉 Analysis complete!

Results: 5 yearly P-values calculated
  Year 1: P5 = 1.05 of P50
  Year 1: P10 = 1.04 of P50
  Year 1: P50 = 1.00 of P50
...

Yearly P50 Deviations to first year P50 value:
  Year 1: 1.00
  Year 2: 0.98
...

Histogram data showing distribution of simulations across uncertainty ranges:
  Year 1: 0 0 0 0 1 15 45 65 75 120 85 77 59 39 21 5 1 0 0 0
  Year 2: 0 0 0 0 2 18 42 68 71 115 89 73 61 35 18 8 0 0 0 0
...
```

---

## Development Setup Instructions

### Option 1: Development Container (Recommended)

The easiest way to get started with development from this codebase is by using the provided development container:

1. **Prerequisites**:
   - Docker Desktop
   - VS Code with Dev Containers extension

2. **Setup**:
   - Open VS Code
   - Open Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`)
   - Type "Dev Containers: Clone Repository in Container Volume..."
   - Enter the repository URL when prompted
   - VS Code will automatically clone and open the project in the dev container

3. **Verify Setup**:
   - Press `Ctrl+Shift+B` (or `Cmd+Shift+B` on Mac) to open the build menu
   - Select "Full Build" task
   - This will run protocol buffer generation, linting, tests, and package building
   - All steps should complete successfully, confirming your environment is ready

4. **Run the Example**:
   - Press `Ctrl+F5` (or `Cmd+F5` on Mac) to run the example script
   - You'll be prompted to enter your SunSolve credentials
   - The example will demonstrate a complete P90 analysis workflow

### Option 2: Manual Setup

If you prefer a manual installation:

1. **Prerequisites**:
   - Python 3.12+
   - pip and pip-tools

2. **Environment Setup**:
   ```bash
   git clone <repository-url>
   cd pvl_p90_client
   
   # Create virtual environment
   python -m venv .venv
   source .venv/bin/activate  # On Windows: .venv\Scripts\activate
   
   # Install pip-tools
   pip install pip-tools
   
   # Compile and install dependencies
   pip-compile requirements-dev.in  # This also compiles requirements.in as a dependency
   pip-sync requirements-dev.txt
   ```

3. **Build Protocol Buffers**:
   ```bash
   chmod +x scripts/build-proto.sh
   ./scripts/build-proto.sh
   ```

4. **Verify Installation**:
   ```bash
   # Run linting checks
   ./scripts/lint.sh
   
   # Run example (requires SunSolve credentials)
   python src/example.py
   ```

---

## Building the Package

### Building for Distribution

To build the Python package for distribution:

```bash
# Build both wheel and source distribution
./scripts/build-package.sh
```

This script will:
1. Validate the version and set up build environment
2. Clean previous builds
3. Build protocol buffers if needed
4. Create both wheel (`.whl`) and source (`.tar.gz`) distributions
5. Place output files in `build/dist/`

### Build Output

After building, you'll find the distribution files in:

```
build/dist/
├── sunsolve_p90_client-<version>-py3-none-any.whl  # Wheel package
└── sunsolve_p90_client-<version>.tar.gz           # Source distribution
```

### Installing the Built Package

```bash
# Install the wheel package locally
pip install build/dist/sunsolve_p90_client-*.whl

# Or install in development mode (editable install)
pip install -e .
```

### Version Management

The package version is automatically determined from:
- `VERSION` file for the base version
- Build number from `BITBUCKET_BUILD_NUMBER` environment variable (CI/CD)
- Branch information for development builds

For local development builds, the version will be `<base_version>+local` format.

---

## Development Tools

### Code Quality

The project uses modern, fast linting tools:

```bash
# Run all linting checks
./scripts/lint.sh

# Auto-fix formatting and linting issues
./scripts/lint.sh -fix
```

**Tools Used:**

- **Ruff**: Ultra-fast Python linter and formatter (replaces Black, isort, Flake8, Bandit, Pylint)
- **MyPy**: Static type checking
- **Custom Test Naming Validator**: Enforces GIVEN_WHEN_THEN test method naming convention

**Pre-commit Hooks:**
```bash
# Install pre-commit hooks (runs linting before commits)
pre-commit install

# Test a commit message format
echo "SUN-4189 Add new feature" | python scripts/check-commit-message.py /dev/stdin
```

### VS Code Tasks

Available tasks in VS Code (Ctrl+Shift+P → Tasks: Run Task):

- **Run Linter (Check Only)**: Check code quality without making changes  
- **Fix Linting Issues**: Auto-fix formatting and import issues
- **Validate Test Naming**: Check test method naming conventions
- **Build Protocol Buffers**: Regenerate protobuf files
- **Run Tests**: Execute all tests with coverage
- **Run Tests with Coverage**: Execute tests with detailed coverage reports
- **Clean DevEnv**: Remove all ignored files (caches, build artifacts, compiled requirements) - follow with container rebuild

### VS Code Launch Configurations

Available launch configurations for running and debugging (Ctrl+Shift+D or F5):

- **Run Example Script**: Execute the example P90 analysis with interactive authentication
- **Run Example (Debug)**: Debug the example script with breakpoints and step-through debugging
- **Python Debugger: Current File**: Debug the currently open Python file
- **Debug pytest Tests**: Debug all tests with breakpoints
- **Debug Current Test File**: Debug the currently open test file

### Testing

The project includes comprehensive test coverage using pytest:

#### VS Code Test Explorer

The devcontainer is configured to automatically discover tests in VS Code:

1. **Test Discovery**: Tests appear in the VS Code Test Explorer sidebar
2. **Run Individual Tests**: Click the play button next to any test
3. **Debug Tests**: Right-click and select "Debug Test"
4. **Run All Tests**: Use the play button at the top of the Test Explorer

#### Command Line Testing

```bash
# Run all tests
python -m pytest tests/ -v

# Run tests with coverage
python -m pytest tests/ --cov=src --cov-report=term-missing

# Run specific test file
python -m pytest tests/test_helpers/test_request_helpers.py -v

# Run specific test class
python -m pytest tests/test_helpers/test_request_helpers.py::TestBuildWeibullErrorFunction -v

# Run specific test
python -m pytest tests/test_helpers/test_request_helpers.py::TestBuildWeibullErrorFunction::test_basic_weibull_creation -v
```

#### VS Code Testing Tasks

Use VS Code tasks for common testing workflows:

- **Run Tests**: Execute all tests with verbose output
- **Run Tests with Coverage**: Generate HTML coverage reports in `htmlcov/`
- **Run Specific Test File**: Run the currently open test file

#### Test Structure

```
tests/
├── conftest.py                    # Shared fixtures and configuration
├── test_helpers/
│   └── test_request_helpers.py    # Tests for helper functions
└── __init__.py

Current Coverage: 27 tests covering:
- Weibull error function creation and validation
- Gaussian error function creation
- Module and system info builders
- Utility functions (temperature, angle conversions)
- Weather data loading and error handling
- Integration tests with protobuf message validation
```

### Protocol Buffer Management

```bash
# Rebuild protobuf files (automatic import fixing included)
./scripts/build-proto.sh

# Manual protobuf compilation
python -m grpc_tools.protoc --proto_path=protos \
    --python_out=src/pvl_p90_client/grpcclient \
    --grpc_python_out=src/pvl_p90_client/grpcclient \
    protos/*.proto
```

---

## Project Structure

```
pvl_p90_client/
├── .devcontainer/                     # VS Code dev container configuration
├── .vscode/                           # VS Code workspace settings
├── data/
│   └── sydney.pvw                     # Sample weather data
├── protos/                            # Protocol buffer definitions
├── scripts/                           # Build and utility scripts
├── src/
│   ├── pvl_p90_client/
│   │   ├── client/                    # Main client class
│   │   ├── grpcclient/                # Auto-generated protobuf files
│   │   └── helpers/                   # Authentication and request utilities
│   └── example.py                     # Complete usage example
├── tests/                             # Test suite
│   ├── test_client/
│   ├── test_helpers/
│   └── conftest.py                    # Shared test fixtures
├── .pre-commit-config.yaml            # Pre-commit hooks configuration
├── bitbucket-pipelines.yml            # CI/CD pipeline configuration
├── pyproject.toml                     # Project configuration
├── requirements-dev.in                # Development dependencies (source)
├── requirements.in                    # Core dependencies (source)
└── VERSION                            # Version file
```

---

## Dependencies

**Core Dependencies**:

- `grpcio` & `grpcio-tools`: gRPC communication
- `protobuf`: Protocol buffer serialization
- `requests`: HTTP authentication
- `PyJWT`: Token handling

**Development Dependencies**:

- `ruff`: Ultra-fast Python linter and formatter
- `mypy`: Type checking
- `pre-commit`: Git commit quality enforcement  
- `pip-tools`: Dependency management

---

## Troubleshooting

### Common Issues

**Import Errors**:
```bash
# Rebuild protocol buffers
./scripts/build-proto.sh
```

**Authentication Failures**:

- Verify SunSolve credentials
- Check network connectivity
- Remove `.pvlToken` file to force re-authentication

**Permission Denied Errors**:

If you encounter a "Permission denied" or HTTP 403 error during analysis:

- Verify your account has an active **Uncertainty Analysis subscription**
- Ensure your account has API access permissions enabled
- Contact SunSolve support to confirm subscription status
- Check that your subscription includes P90/uncertainty analysis features

**Linting Errors**:
```bash
# Auto-fix most issues
./scripts/lint.sh -fix
```

### Getting Help

For issues specific to this client library, check:

1. Error messages and stack traces
2. Linting output for code quality issues
3. VS Code Problems panel for real-time feedback

For SunSolve service issues:

- Verify account access and permissions
- Check service status and availability