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
requestsandpandas.
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,
})
Using FilterBuilder (Recommended for complex queries)
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
| Feature | Filter Key | Description |
|---|---|---|
| Polymer | polymer | Case-insensitive polymer name (e.g., PAN, PVP) |
| Solvent | solvent | Case-insensitive solvent name (e.g., DMF) |
| Voltage | voltageMin / voltageMax | Voltage range in kV |
| Flow Rate | flowRateMin / flowRateMax | Flow rate range in mL/h |
| Distance | tipDistMin / tipDistMax | Tip-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
versionis given (e.g."v1.0.0"). Columns are automatically flattened into research-ready names (e.g.,experiment_id,polymer(s),solution_concentration). Equivalent todownload_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_UPDATEback toPENDING.
status(submission_id) / get_submission_status(submission_id)
- Returns:
dict— the full submission, including each record's currentstatus - 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"). Omitstatusfor 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()