Skip to main content

Python Client Guide

The Electrospinning Data Python client is a production-ready library designed for researchers to interact programmatically with our dataset. It focuses on simplicity, reproducibility, and seamless integration with the Python data science stack (pandas).

[!TIP] For a complete, table-based listing of every method (Python and REST), see the Complete API Reference.


Installation

Install the package via pip:

pip install electrospinning-data-client

[!NOTE] The package requires Python 3.8+ and depends on requests and pandas.


Quick Start

Client is the main entry point:

from electrospinning_data_client import Client

client = Client(token="esd_pat_your_token_here") # omit token for read-only use

df = client.download() # latest dataset as a pandas DataFrame
df = client.search(filters={"polymer": "PAN"}) # filtered query

print(f"Retrieved {len(df)} records")
print(df.head())

Every method has a short, everyday name (submit, update, status, download, search, records, record_ids, versions) that's a thin wrapper around a longer, more descriptive one you may already be using (submit_experiment, update_experiment, get_submission_status, download_latest/download_version, list_my_records, list_my_record_ids, get_versions) — both spellings call exactly the same code. Nothing old is removed or deprecated — if you have existing code using the longer names or ElectrospinningDataClient, it keeps working unchanged; see Migrating from the old API below.

The original top-level convenience functions are still available too, if you prefer a one-shot, no-object-to-manage style:

import electrospinning_data_client as edc

df = edc.load_latest_dataset()

Filtering Data

The client allows you to apply any of the supported API filters directly as a dictionary or using the fluent FilterBuilder for better type safety and readability.

Using a Dictionary

from electrospinning_data_client import Client

client = Client()

df = client.search(filters={
"polymer": "PAN",
"voltageMin": 20,
"voltageMax": 30,
})

The FilterBuilder provides an ergonomic way to construct complex queries.

from electrospinning_data_client import Client, FilterBuilder

client = Client()

filters = (FilterBuilder()
.polymer("PAN")
.voltage(min_val=20, max_val=30)
.build())

df = client.search(filters=filters)

search(filters=...) and download(filters=...) are equivalent — search just reads better when filtering is the point of the call:

df = client.download(filters=filters) # same result as client.search(filters=filters)

Common Filter Keys

FeatureFilter KeyDescription
PolymerpolymerCase-insensitive polymer name (e.g., PAN, PVP)
SolventsolventCase-insensitive solvent name (e.g., DMF)
VoltagevoltageMin / voltageMaxVoltage range in kV
Flow RateflowRateMin / flowRateMaxFlow rate range in mL/h
DistancetipDistMin / tipDistMaxTip-to-collector distance in cm

Versioned Downloads

For reproducible research, we recommend referencing specific dataset versions.

from electrospinning_data_client import Client

client = Client()

# 1. List available versions
for v in client.versions():
# Access attributes directly (snake_case)
print(f"Version: {v.version_identifier}, Count: {v.record_count}")

# 2. Download a specific, reproducible snapshot
df_v1 = client.download(version="v1.0.0")

# 3. ...optionally filtered too
df_v1_pan = client.search(filters={"polymer": "PAN"}, version="v1.0.0")

Submitting Data

Reading and downloading the dataset requires no authentication. Submitting or updating experiment records does — you'll need a personal access token from your profile settings. See Authentication & API Tokens for how to create one.

from electrospinning_data_client import Client, AuthenticationError, APIError

client = Client(token="esd_pat_your_token_here")

payload = {
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"researchMetadata": {"publicationTitle": "My study", "doi": "10.1234/example"},
"experimentData": [{"polymerProperty": {}, "processParameter": {}}],
}

try:
result = client.submit(payload)
for record in result["records"]:
if record["status"] == "NEEDS_UPDATE":
print(f"Record {record['recordId']} saved but incomplete, missing: {record['missingFields']}")
else:
print(f"Record {record['recordId']} submitted as {record['status']}")
except AuthenticationError:
print("No token configured - create one from your profile settings first.")
except APIError as e:
print(f"Submission rejected ({e.status_code}): {e}")

client.update(experiment_id, payload) works the same way for updating an experiment you previously submitted. Both methods accept a plain dict matching the submission JSON shape used by the website's submission form and return a parsed JSON dict; submitted records go through the same moderation queue as web-form submissions.

Incomplete submissions (NEEDS_UPDATE)

If a token-authenticated submission is missing a mandatory scientific field (currently: polymer information, or a solvent component's name), it's saved, not rejected — with status NEEDS_UPDATE instead of PENDING. This is a normal, successful result, not an exception: check result["records"][i]["status"] and ["missingFields"] rather than wrapping the call in a try/except for this case.

# Missing polymerProperty entirely - still succeeds:
result = client.submit({
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"researchMetadata": {"publicationTitle": "My study"},
"experimentData": [{"processParameter": {"voltage": 20}}],
})
record = result["records"][0]
assert record["status"] == "NEEDS_UPDATE"
assert "polymerProperty" in record["missingFields"][0]

# Later, fill in the missing field and resubmit:
client.update(record["recordId"], {
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"experimentData": [{"polymerProperty": {"polymerComponents": [{"polymerName": "PVA"}]}}],
})
# -> status flips back to "PENDING" and re-enters the moderation queue

A field that's provided but invalid (e.g. a misspelled polymer name) always raises APIError with a 400 status, in every case — see Record statuses & incomplete submissions for the full picture, including the GET /data/statuses endpoint and error codes.

Checking submission status

status = client.status(result["submissionId"])
print(status["status"]) # e.g. "NEEDS_UPDATE", "PENDING", "APPROVED", "REJECTED", or "MIXED"

Listing your own records

Query your submitted records by status — separately or all together. This is the fastest way to find everything that still NEEDS_UPDATE across every submission you've made, not just the one you just submitted:

# Just the ids - cheap, good for a quick check
needs_update_ids = client.record_ids(status="NEEDS_UPDATE")

# Full record data for the same filter
needs_update_records = client.records(status="NEEDS_UPDATE")

# Every record you've ever submitted, any status
all_my_records = client.records()

for record in needs_update_records:
print(record["recordId"], "is missing fields - fill them in with update()")

Both methods accept status as one of "PENDING", "APPROVED", "REJECTED", "NEEDS_UPDATE", or None (the default) for every status combined.


Migrating from the old API

There's no rush and no deadline — nothing below is deprecated, and old code keeps working without any changes. This table is only here if you'd like to adopt the shorter style going forward:

Old (still works)New
ElectrospinningDataClient(api_token=...)Client(token=...)
client.submit_experiment(payload)client.submit(payload)
client.update_experiment(id, payload)client.update(id, payload)
client.get_submission_status(id)client.status(id)
client.download_latest(filters=None)client.download()
client.download_version(version, filters=None)client.download(version=version)
client.download_latest(filters={...})client.search(filters={...})
client.list_my_records(status=None)client.records(status=None)
client.list_my_record_ids(status=None)client.record_ids(status=None)
client.get_versions()client.versions()

ElectrospinningDataClient is not a separate, older implementation — it's a plain alias for the exact same Client class, so isinstance() checks and subclassing against the old name keep working too.


API Reference

Client (alias: ElectrospinningDataClient)

The core class for all interactions.

__init__(base_url=None, timeout=60, verify=True, token=None, api_token=None, api_base_url=None)

Initialize the client. Defaults to the official production API. Uses connection pooling via requests.Session. Pass token (a personal access token from your profile settings) to use submit/update; api_token is kept as an alias for existing code. api_base_url lets you override the root URL write requests are sent to (derived from base_url by default).

download(version=None, filters=None)

  • Returns: pandas.DataFrame
  • Description: Latest dataset snapshot by default, or a specific version if version is given (e.g. "v1.0.0"). Columns are automatically flattened into research-ready names (e.g., experiment_id, polymer(s), solution_concentration). Equivalent to download_latest/download_version.

search(filters=None, version="latest")

  • Returns: pandas.DataFrame
  • Description: Filtered dataset query. Equivalent to download(filters=filters, version=version) — use whichever name reads better at the call site.

submit(record) / submit_experiment(payload)

  • Returns: dict{"message", "submissionId", "records": [{"recordId", "status", "missingFields"}, ...]}
  • Requires: a token
  • Description: Submits a new experiment record for moderation. Missing mandatory fields don't raise — the record is saved with status "NEEDS_UPDATE" instead. See Submitting Data above.

update(id, record) / update_experiment(experiment_id, payload)

  • Returns: dict{"recordId", "status", "missingFields", "message"}
  • Requires: a token
  • Description: Updates an experiment record you previously submitted. Completing all mandatory fields flips NEEDS_UPDATE back to PENDING.

status(submission_id) / get_submission_status(submission_id)

  • Returns: dict — the full submission, including each record's current status
  • Requires: a token
  • Description: Retrieves the current state of a submission you own.

records(status=None) / list_my_records(status=None)

  • Returns: list[dict] — full record data for each match
  • Requires: a token
  • Description: Lists your own submitted records, optionally filtered to one status ("PENDING", "APPROVED", "REJECTED", "NEEDS_UPDATE"). Omit status for every record combined.

record_ids(status=None) / list_my_record_ids(status=None)

  • Returns: list[int]
  • Requires: a token
  • Description: Same filtering as records, but returns only record ids — cheaper when you just need to know what to look at.

versions() / get_versions()

  • Returns: List[VersionInfo]
  • Description: Retrieves metadata for all available version snapshots.

export_file(output_path, export_format='xlsx', version='latest', filters=None)

  • Description: Downloads the dataset in a specified format (xlsx, json, zip) directly to a file.
  • Example:
    client.export_file("data.xlsx", export_format="xlsx", filters={"polymer": "PAN"})

close()

  • Description: Closes the underlying transport session. Can be used manually or via a context manager if using the client directly.

Working with Pandas

Once the data is in a DataFrame, you can leverage all of pandas' power for analysis. The client ensures that physical quantities are numeric and categorical data is standardized.

from electrospinning_data_client import Client
import matplotlib.pyplot as plt

client = Client()
df = client.download()

# The client uses idiomatic snake_case column names
pan_data = df[df['polymer(s)'] == 'PAN']

# Calculate average fiber diameter (numeric column)
avg_diameter = pan_data['fiber_diameter'].mean()
print(f"Average PAN Fiber Diameter: {avg_diameter:.2f} nm")

# Simple visualization
df.plot(kind='scatter', x='voltage', y='fiber_diameter')
plt.show()