Introduction

equser is the open-source Python interface to EQ Wave power quality data and sensors. It provides tools for loading, analyzing, and visualizing high-resolution electrical measurements.

What equser does

equser can work with EQ Wave data in several ways:

  • Load and analyze Parquet files stored by an EQ gateway, including continuous waveform (CPOW) and power monitoring (PMon) data
  • Connect directly to an EQ Wave sensor to collect PMon data in real time
  • Capture waveform snapshots on demand via an EQ gateway
  • Query gateway APIs for historical data, events, and live streams
  • Visualize power quality data with publication-ready matplotlib plots

Data formats

FormatDescriptionSample RateFile Pattern
CPOWContinuous Point-on-Wave waveforms (7 channels: VA, VB, VC, IA, IB, IC, IN)32 kHzYYYYMMDD_HHMMSS.parquet
PMon10/12-cycle RMS power quality summaries (voltage, current, power, frequency)~5 HzYYYYMMDD_HHMM.parquet

Quick install

pip install equser              # Data loading and analysis
pip install equser[analysis]    # Add plotting and API client
pip install equser[jupyter]     # Full notebook environment

Get started

See the Installation guide, then follow the Quick Start tutorial.

The broader platform

equser is one component of the Energy Quotient platform for continuous waveform intelligence. For the full capabilities, including real-time CPOW visualization through EQ Sight and power system intelligence through EQ Syntropy, see the platform overview.

Installation

Requirements

  • Python 3.10 or later
  • Linux (for hardware integration features)

Install from source

equser uses a tiered dependency model. Install only what you need.

Base (data loading and analysis)

pip install equser

Includes: numpy, pyarrow, pyyaml, argcomplete, colorlog. Enough to load CPOW/PMon files, run waveform analysis, and use the CLI with tab completion and colored output.

Plotting and API client

pip install equser[analysis]

Adds: matplotlib, requests, websocket-client. Enables equser.plotting, equser.api, and WebSocket streaming.

Jupyter notebook environment

pip install equser[jupyter]

Superset of [analysis]. Adds: jupyterlab, duckdb, ipywidgets, ipykernel, nbconvert. Full interactive notebook environment for data exploration.

Live sensor acquisition

pip install equser[daq]

Adds: avro, fastavro. Required for equser pmon acquire to connect to EQ Wave sensor hardware.

Full installation

pip install equser[full]

All optional extras.

Dependency tiers

ExtraDescriptionKey Packages
(base)Data loading, analysis, CLInumpy, pyarrow, pyyaml, argcomplete, colorlog
[daq]Live sensor acquisitionavro, fastavro
[analysis]Plotting + API clientmatplotlib, requests, websocket-client
[jupyter]Full notebook environment[analysis] + jupyterlab, duckdb, ipywidgets
[full]All of the above-

Quick Start

Load CPOW waveform data

CPOW (Continuous Point-on-Wave) files contain high-resolution waveform captures at 32 kHz across 7 channels.

from equser.data import load_cpow_scaled

result = load_cpow_scaled('20250623_075056.parquet')

# Scaled voltage and current arrays (numpy float64)
print(f"Voltage A peak: {result['VA'].max():.1f} V")
print(f"Current A peak: {result['IA'].max():.3f} A")
print(f"Start time: {result['start_time']}")
print(f"Sample rate: {result['sample_rate']} Hz")

The function handles both data formats automatically:

  • int32 (current format): raw ADC counts scaled by vscale/iscale from Parquet metadata.
  • float (legacy format): values already in volts/amps.

Load PMon summary data

PMon (Power Monitor) files contain 10/12-cycle RMS measurements (~200 ms intervals).

from equser.data import load_pmon

table = load_pmon('20250623_0750.parquet')
print(table.column_names)
# ['time_us', 'FREQ', 'AVRMS', 'BVRMS', 'CVRMS', 'AIRMS', ...]

Analyze zero crossings

Zero-crossing detection is useful for frequency estimation, cycle extraction, and identifying waveform distortions.

import numpy as np
from equser.data import load_cpow_scaled, SAMPLE_RATE_HZ
from equser.analysis import find_zero_crossings

result = load_cpow_scaled('cpow_data.parquet')
time = np.arange(len(result['VA'])) / SAMPLE_RATE_HZ

crossings, indices = find_zero_crossings(result['VA'], time)
print(f"Found {len(crossings)} zero crossings")

# Estimate frequency from crossing intervals
periods = np.diff(crossings)
freq = 1.0 / np.mean(periods)
print(f"Estimated frequency: {freq:.2f} Hz")

Extract complete cycles

from equser.analysis import extract_complete_cycles

cycles = extract_complete_cycles(result['VA'], time, [0.0, 0.05], num_cycles=1)

for cycle_time, cycle_signal, start, end in cycles:
    duration_ms = (end - start) * 1000
    print(f"Cycle at {start:.6f}s, duration: {duration_ms:.2f} ms")

Plot data (requires [analysis])

from equser.plotting import PowerMonitorPlotter, WaveformPlotter

# PMon data
plotter = PowerMonitorPlotter()
plotter.plot_file('pmon_data.parquet')

# CPOW waveforms
wf_plotter = WaveformPlotter()
wf_plotter.plot_file('cpow_data.parquet')

Query a gateway (requires [analysis])

from equser.api import SynapseClient

client = SynapseClient('http://gateway:8080')
devices = client.list_devices()
table = client.get_pmon_data(devices[0]['id'])

# SQL queries
rows = client.query_sql("SELECT * FROM pmon ORDER BY time_us DESC", limit=10)

Stream real-time data (requires [analysis])

from equser.api import connect_cpow_stream

for batch_or_gap in connect_cpow_stream('http://gateway:8080'):
    if isinstance(batch_or_gap, dict):
        print(f"Gap: {batch_or_gap['skipped_samples']} samples")
    else:
        print(f"Batch: {batch_or_gap.num_rows} rows")

CLI tools

# Start power monitoring (requires [daq] + EQ Wave hardware)
equser pmon acquire -c config.yaml

# Convert Avro files to Parquet (requires [daq])
equser pmon convert data/*.avro --remove

# Plot data file (requires [analysis])
equser plot data.parquet

Configuration

equser uses YAML configuration files for sensor and acquisition settings. The configuration system follows XDG conventions and supports multiple lookup locations.

Configuration lookup order

equser searches for configuration files in this order:

  1. EQUSER_CONFIG environment variable (if set)
  2. ./equser.yaml (current directory)
  3. ~/.config/equser/config.yaml (XDG config)
  4. /etc/equser/config.yaml (system-wide)

The first file found is used.

Example configuration

sensor:
  address: "192.168.10.10"
  port: 1535

pmon:
  connection:
    retry_delay: 3
  parquet:
    interval: 86400
    compression:
      method: ZSTD
      level: 4

Configuration sections

sensor

Connection settings for EQ Wave sensor hardware.

KeyTypeDefaultDescription
addressstring"192.168.10.10"Sensor IP address
portint1535Sensor TCP port

pmon

Power monitor acquisition settings.

KeyTypeDefaultDescription
connection.retry_delayint3Seconds between reconnect attempts
parquet.intervalint86400Seconds per Parquet file (86400 = 24 hours)
parquet.compression.methodstring"ZSTD"Compression method (ZSTD, SNAPPY, GZIP, or NONE)
parquet.compression.levelint4Compression level

Data paths

Default paths follow XDG conventions:

PathContents
~/.local/share/equser/pmon/PMon Parquet files
~/.config/equser/config.yamlUser configuration

On EQ Synapse gateways, data is stored at:

PathContents
/var/lib/eq-synapse/data/pmon/PMon files (YYYYMMDD_HHMM.parquet)
/var/lib/eq-synapse/data/cpow/CPOW files (YYYYMMDD_HHMMSS.parquet)

Programmatic access

from equser.core.config import load_config, get_sensor_address

config = load_config()
address = get_sensor_address(config)
print(f"Sensor at {address}")

Data Loading

Module: equser.data Dependencies: base (numpy, pyarrow)

Load CPOW and PMon Parquet files with automatic scaling and timestamp parsing.

CPOW data

load_cpow_scaled(file_path) -> dict

Load a CPOW Parquet file and return scaled voltage/current arrays.

Handles both formats automatically:

  • int32 (current): raw ADC counts scaled by vscale/iscale from Parquet user metadata.
  • float (legacy): values already in V/A; scaling factors are 1.0.

Returns a dict with:

KeyTypeDescription
tablepa.TableRaw PyArrow Table
VA, VB, VCnp.ndarrayScaled voltage arrays (float64)
IA, IB, IC, INnp.ndarrayScaled current arrays (float64)
vscalefloatVoltage scaling factor applied
iscalefloatCurrent scaling factor applied
start_timedatetime or NoneParsed from metadata
sample_rateintAlways 32000
from equser.data import load_cpow_scaled

result = load_cpow_scaled('20250623_075056.parquet')
print(f"Peak voltage A: {result['VA'].max():.1f} V")
print(f"Samples: {len(result['VA'])}")

load_cpow(file_path) -> pa.Table

Load a CPOW Parquet file as a raw PyArrow Table with no scaling applied. Use this when you need the raw integer ADC values or want to handle scaling yourself.

Constants

ConstantValueDescription
SAMPLE_RATE_HZ32000CPOW sample rate
CHANNELS['VA', 'VB', 'VC', 'IA', 'IB', 'IC', 'IN']Channel names
NEUTRAL_CT_RATIO30Neutral CT sensitivity ratio vs. phase CTs

PMon data

load_pmon(file_path) -> pa.Table

Load a PMon Parquet file as a PyArrow Table.

PMon files contain 10/12-cycle RMS measurements (10 cycles for 50 Hz grids, 12 cycles for 60 Hz). Common columns include:

ColumnDescription
time_usTimestamp in microseconds
FREQLine frequency (Hz)
AVRMS, BVRMS, CVRMSPhase RMS voltage
AIRMS, BIRMS, CIRMSPhase RMS current
NIRMSNeutral RMS current
AWATT, BWATT, CWATTPhase active power
from equser.data import load_pmon

table = load_pmon('20250623_0750.parquet')
freq = table.column('FREQ').to_numpy()
print(f"Mean frequency: {freq.mean():.3f} Hz")

Timestamp parsing

parse_start_time(s) -> datetime

Parse an ISO 8601 timestamp string from CPOW metadata. Handles nanosecond precision by truncating to microseconds (Python datetime limit).

from equser.data import parse_start_time

dt = parse_start_time("2025-06-23T07:50:56.123456789Z")
print(dt)  # 2025-06-23 07:50:56.123456+00:00

parse_filename_timestamp(filename) -> datetime | None

Extract a timestamp from an EQ data filename pattern.

Supports:

  • YYYYMMDD_HHMM (PMon files)
  • YYYYMMDD_HHMMSS (CPOW files)
from equser.data import parse_filename_timestamp

dt = parse_filename_timestamp("20250623_075056.parquet")
print(dt)  # 2025-06-23 07:50:56

Waveform Analysis

Module: equser.analysis Dependencies: base (numpy only)

Functions for zero-crossing detection, cycle extraction, and waveform analysis.

find_zero_crossings(signal, time_array)

Find negative-to-positive zero crossings in a signal using linear interpolation between adjacent samples.

Args:

  • signal (ndarray): Voltage or current waveform array.
  • time_array (ndarray): Corresponding time array (same length).

Returns: (crossing_times, crossing_indices)

  • crossing_times: interpolated times of each zero crossing
  • crossing_indices: index of the sample just before each crossing
import numpy as np
from equser.data import load_cpow_scaled, SAMPLE_RATE_HZ
from equser.analysis import find_zero_crossings

result = load_cpow_scaled('cpow_data.parquet')
time = np.arange(len(result['VA'])) / SAMPLE_RATE_HZ

crossings, indices = find_zero_crossings(result['VA'], time)
print(f"Found {len(crossings)} zero crossings")

# Frequency estimation
periods = np.diff(crossings)
print(f"Mean frequency: {1.0 / np.mean(periods):.2f} Hz")
print(f"Frequency std:  {np.std(1.0 / periods):.4f} Hz")

extract_complete_cycles(signal, time_array, start_times, num_cycles=1)

Extract complete AC cycles (zero-crossing to zero-crossing) starting from specified times.

Args:

  • signal (ndarray): Waveform array.
  • time_array (ndarray): Time array.
  • start_times (list of float): Times to start extracting from.
  • num_cycles (int): Number of cycles per start time (default 1).

Returns: List of tuples (cycle_time, cycle_signal, start, end) where:

  • cycle_time: time array normalized to start at 0
  • cycle_signal: signal values for the window
  • start: actual start time (first zero crossing)
  • end: actual end time (last zero crossing)

Returns an empty list if there are not enough zero crossings for the requested number of cycles.

from equser.analysis import extract_complete_cycles

# Extract single cycles at 3 different times
cycles = extract_complete_cycles(result['VA'], time, [0.0, 0.05, 0.1])

for cycle_time, cycle_signal, start, end in cycles:
    duration_ms = (end - start) * 1000
    peak = np.max(np.abs(cycle_signal))
    print(f"Cycle at {start:.4f}s: {duration_ms:.2f} ms, peak {peak:.1f} V")

plot_extracted_cycles(signal_dict, time_array, start_times, ...)

Plot extracted cycles for one or more phases side-by-side. Requires matplotlib ([analysis] extra).

Args:

  • signal_dict: Dict mapping phase names to arrays (e.g. {'VA': va, 'VB': vb}), or a single array (treated as {'VA': array}).
  • time_array: Time array.
  • start_times: Times to extract cycles from.
  • num_cycles (int): Cycles per window (default 1).
  • epoch_start_time (datetime, optional): Absolute start time for labels.

Returns: (fig, axes, window_times)

from equser.analysis.waveform import plot_extracted_cycles

fig, axes, windows = plot_extracted_cycles(
    {'VA': result['VA'], 'VB': result['VB'], 'VC': result['VC']},
    time,
    start_times=[0.0, 0.1, 0.2],
    num_cycles=2,
    epoch_start_time=result['start_time'],
)
fig.savefig('cycles.png', dpi=150, bbox_inches='tight')

Plotting

Module: equser.plotting Dependencies: [analysis] extra (matplotlib)

Static matplotlib plots for PMon and CPOW data. Install with:

pip install equser[analysis]

PowerMonitorPlotter

Plot power monitor (PMon) time-series data.

from equser.plotting import PowerMonitorPlotter

plotter = PowerMonitorPlotter()

# Plot from a file
plotter.plot_file('pmon_data.parquet')

# Plot from a PyArrow Table
from equser.data import load_pmon
table = load_pmon('pmon_data.parquet')
plotter.plot(table)

Methods

MethodDescription
plot_file(path)Load and plot a PMon Parquet file
plot(table)Plot from a PyArrow Table

Output

Generates a multi-panel figure with:

  • Frequency vs. time
  • Phase RMS voltages vs. time
  • Phase RMS currents vs. time
  • Phase active power vs. time

Color scheme follows standard power systems convention (black/red/blue for phases A/B/C).

WaveformPlotter

Plot CPOW waveform captures.

from equser.plotting import WaveformPlotter

plotter = WaveformPlotter()

# Plot from a file
plotter.plot_file('cpow_data.parquet')

Methods

MethodDescription
plot_file(path)Load and plot a CPOW Parquet file

Output

Generates voltage and current waveform plots with proper scaling. The neutral current is automatically adjusted by the CT sensitivity ratio (NEUTRAL_CT_RATIO = 30).

Color schemes

Both plotters support named color schemes:

SchemeDescription
"standard"Black/Red/Blue (default, power systems convention)
"colorblind"Colorblind-friendly palette
"monochrome"Grayscale with line style variation

Saving plots

All plotters return matplotlib figure objects. Save in any format:

fig = plotter.plot_file('data.parquet')
fig.savefig('output.png', dpi=150, bbox_inches='tight')
fig.savefig('output.svg')
fig.savefig('output.pdf')

API Client

Module: equser.api Dependencies: [analysis] extra (requests, websocket-client)

REST and WebSocket clients for EQ Synapse gateways. Install with:

pip install equser[analysis]

SynapseClient

REST client for the EQ Synapse API (Actix-web server at port 8080).

from equser.api import SynapseClient

client = SynapseClient('http://gateway:8080')

list_devices() -> list[dict]

List all registered devices.

devices = client.list_devices()
for d in devices:
    print(f"{d['id']}: {d.get('name', 'unnamed')}")

get_pmon_data(device_id, **params) -> pa.Table

Fetch PMon data as an Arrow Table.

table = client.get_pmon_data('wave-001')
print(f"Rows: {table.num_rows}, Columns: {table.column_names}")

Optional query parameters: start_time, end_time, metrics, limit.

get_cpow_data(device_id, **params) -> pa.Table

Fetch CPOW waveform data as an Arrow Table.

table = client.get_cpow_data('wave-001')

Optional query parameters: start_time, end_time, limit.

get_events(device_id=None, limit=100) -> list[dict]

Fetch recent power quality events.

events = client.get_events(limit=10)
for e in events:
    print(f"{e['timestamp']}: {e['type']}")

query_sql(query, device_id=None, limit=None) -> list[dict]

Execute a SELECT query via the SQL endpoint. Only SELECT statements are allowed by the server.

rows = client.query_sql(
    "SELECT time_us, FREQ, AVRMS FROM pmon ORDER BY time_us DESC",
    limit=10,
)

WebSocket streaming

connect_cpow_stream(gateway_url) -> Generator

Connect to the CPOW waveform WebSocket and yield data in real time.

Each binary message is an Arrow IPC RecordBatch (~512 rows, 16 ms at 32 kHz). Text messages are JSON gap markers indicating dropped samples.

from equser.api import connect_cpow_stream

for item in connect_cpow_stream('http://gateway:8080'):
    if isinstance(item, dict):
        print(f"Gap: {item['skipped_samples']} samples")
    else:
        # item is a pyarrow.RecordBatch
        va = item.column('VA').to_numpy()
        print(f"Batch: {item.num_rows} rows, VA peak: {va.max()}")

connect_spectral_stream(device_id, ...) -> Generator

Connect to the spectral analysis WebSocket and yield JSON frames.

Args:

ParameterDefaultDescription
device_id(required)Device identifier
phase'va'Channel: va, vb, vc, ia, ib, ic
fft_size4096FFT window size (power of 2)
update_rate10.0Frames per second
freq_min0Minimum frequency (Hz)
freq_max3000Maximum frequency (Hz)
from equser.api import connect_spectral_stream

for frame in connect_spectral_stream('wave-001', phase='va', fft_size=8192):
    print(f"Spectral frame: {len(frame.get('magnitudes', []))} bins")

API endpoints

The EQ Synapse server (Actix-web) exposes these endpoints:

EndpointMethodDescription
/api/v1/devicesGETList devices
/api/v1/devices/{id}/pmon/dataGETPMon data (Arrow IPC)
/api/v1/devices/{id}/cpow/dataGETCPOW data (Arrow IPC)
/api/v1/eventsGETPower quality events
/api/v1/events/streamGETEvent stream (SSE)
/api/v1/query/sqlPOSTSQL query (SELECT only)
/api/ws/cpow_streamWSReal-time waveform stream
/api/ws/spectralWSReal-time spectral stream

Data endpoints return Arrow IPC binary format for efficient transfer.

Live Acquisition

Module: equser.pmon Dependencies: [daq] extra (avro, fastavro)

Real-time power monitor data acquisition from EQ Wave sensor hardware. Install with:

pip install equser[daq]

Note: Live acquisition requires physical EQ Wave sensor hardware connected via Ethernet.

PowerMonitor

The main acquisition class. Connects to an EQ Wave sensor, reads 10/12-cycle power quality measurements, and writes them to Parquet files.

from equser.pmon import PowerMonitor

monitor = PowerMonitor(config)
monitor.run()  # Blocks until interrupted

acquire(config_path)

Convenience function to load configuration and start monitoring.

from equser.pmon import acquire

acquire('equser.yaml')

Avro-to-Parquet conversion

Convert legacy Avro data files to Parquet format.

from equser.pmon import convert_avro_to_parquet

convert_avro_to_parquet('input.avro', 'output.parquet')

Schema

The PMon schema defines 10/12-cycle power quality measurements:

from equser.pmon.schema import create_schema

schema, schema_str, field_names = create_schema(num_phases=3)
print(field_names)
# ['time_us', 'FREQ', 'AVRMS', 'BVRMS', 'CVRMS', ...]

Supports 1, 2, or 3-phase configurations. Fields include:

FieldDescription
time_usMeasurement timestamp (microseconds)
FREQLine frequency (Hz)
{A,B,C}VRMSPhase RMS voltage
{A,B,C}IRMSPhase RMS current
NIRMSNeutral RMS current
{A,B,C}WATTPhase active power
{A,B,C}VFUNDPhase fundamental voltage
{A,B,C}IFUNDPhase fundamental current

Error hierarchy

equser.pmon.errors.PMonError
├── ConnectionError    - Sensor connection failures
├── DataError         - Invalid or corrupt data
└── ConfigError       - Configuration problems

CLI

# Start acquisition
equser pmon acquire -c config.yaml

# Convert Avro to Parquet
equser pmon convert data/*.avro --remove

CLI Reference

equser provides a command-line interface for common operations.

Usage

equser <command> [options]

Commands

equser pmon acquire

Start power monitoring from an EQ Wave sensor. Requires [daq] extra and physical sensor hardware.

equser pmon acquire -c config.yaml
OptionDescription
-c, --configPath to YAML configuration file

Reads 10/12-cycle RMS measurements and writes Parquet files to the configured data directory. Runs continuously until interrupted (Ctrl+C).

equser pmon convert

Convert Avro data files to Parquet format. Requires [daq] extra.

equser pmon convert data/*.avro
equser pmon convert data/*.avro --remove
OptionDescription
--removeDelete source Avro files after successful conversion

equser plot

Plot a PMon or CPOW data file. Requires [analysis] extra.

equser plot data.parquet

Automatically detects the file type (PMon vs. CPOW) and uses the appropriate plotter.

equser notebooks list

List the reference notebooks bundled in the equser package.

equser notebooks list

equser notebooks copy

Copy reference notebooks to a directory.

equser notebooks copy --dest ./notebooks
equser notebooks copy --dest ./notebooks --category tutorials
equser notebooks copy --dest ./notebooks --overwrite
OptionDescription
--destDestination directory (default: current directory)
--categoryOnly copy notebooks from this category (tutorials or analysis)
--overwriteOverwrite existing files

equser snapshot

Capture live waveform data from a gateway and save to a Parquet file.

equser snapshot
equser snapshot --host 192.168.1.10 --duration 10 --output capture.parquet
OptionDescription
--hostGateway hostname or IP (default: localhost)
--portGateway port (default: 8080)
--durationCapture duration in seconds (default: 5.0)
--outputOutput Parquet file path (auto-generated if omitted)

Tab completion

Install tab completion with the [cli] extra:

pip install equser[cli]
eval "$(register-python-argcomplete equser)"

Add the eval line to your shell profile for persistent completion.

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

0.1.0 - 2026-02-06

Initial public release. User toolkit for EQ Wave power quality data.

Modules

  • equser.data - Load CPOW and PMon Parquet files with automatic scaling, timestamp parsing
  • equser.analysis - Waveform analysis: zero-crossing detection, AC cycle extraction
  • equser.api - REST and WebSocket clients for EQ Synapse gateways (requires [analysis])
  • equser.plotting - Static matplotlib plots for PMon and CPOW data (requires [analysis])
  • equser.pmon - Live sensor acquisition and Avro-to-Parquet conversion (requires [daq])
  • equser.core - YAML configuration loading, XDG-compliant path resolution
  • equser.utils - Logging with optional color, DateTime with floor-division
  • equser.notebooks - Bundled reference notebooks with list/copy API
  • equser.widgets - Interactive file selector for JupyterLab (requires [jupyter])
  • equser.snapshot - Waveform capture via gateway WebSocket

Dependency Tiers

  • Base: numpy, pyarrow, pyyaml, argcomplete, colorlog (data loading, analysis, CLI)
  • [daq]: avro, fastavro (live sensor acquisition)
  • [analysis]: matplotlib, requests, websocket-client (plotting + API)
  • [jupyter]: [analysis] + jupyterlab, duckdb, ipywidgets, ipykernel, nbconvert
  • [full]: all of the above

CLI

  • equser pmon acquire - Start power monitoring from EQ Wave sensor
  • equser pmon convert - Convert Avro files to Parquet
  • equser plot - Plot PMon or CPOW data files
  • equser notebooks list - List bundled reference notebooks
  • equser notebooks copy - Copy reference notebooks to a directory
  • equser snapshot - Capture live waveform data to a Parquet file

MIT License

Copyright (c) 2026 EQ Systems Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.