Skip to content

Commit a8a65ca

Browse files
committed
Add ds.jobs.list() and standardize docs to use ds variable
Add list_jobs() to fetch Tapis jobs as a pandas DataFrame with optional filtering by app_id and status. Includes 14 tests. Rename client variable to ds across all documentation examples.
1 parent 14e9b9c commit a8a65ca

11 files changed

Lines changed: 468 additions & 183 deletions

File tree

dapi/client.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,21 @@ class DSClient:
4040
Example:
4141
Basic usage with automatic authentication:
4242
43-
>>> client = DSClient()
43+
>>> ds = DSClient()
4444
Enter DesignSafe Username: myuser
4545
Enter DesignSafe Password: [hidden]
4646
Authentication successful.
4747
4848
Using explicit credentials:
4949
50-
>>> client = DSClient(username="myuser", password="mypass")
50+
>>> ds = DSClient(username="myuser", password="mypass")
5151
Authentication successful.
5252
5353
Using a pre-authenticated Tapis client:
5454
5555
>>> tapis = Tapis(base_url="https://designsafe.tapis.io", ...)
5656
>>> tapis.get_tokens()
57-
>>> client = DSClient(tapis_client=tapis)
57+
>>> ds = DSClient(tapis_client=tapis)
5858
"""
5959

6060
def __init__(self, tapis_client: Optional[Tapis] = None, **auth_kwargs):
@@ -196,7 +196,7 @@ def translate_uri_to_path(self, *args, **kwargs) -> str:
196196
str: The corresponding DesignSafe local path (e.g., /home/jupyter/MyData/path).
197197
198198
Example:
199-
>>> local_path = client.files.translate_uri_to_path("tapis://designsafe.storage.default/user/data")
199+
>>> local_path = ds.files.translate_uri_to_path("tapis://designsafe.storage.default/user/data")
200200
>>> print(local_path) # "/home/jupyter/MyData/data"
201201
"""
202202
return files_module.tapis_uri_to_local_path(*args, **kwargs)
@@ -453,7 +453,7 @@ def generate_request(
453453
JobSubmissionError: If job request generation fails.
454454
455455
Example:
456-
>>> job_request = client.jobs.generate_request(
456+
>>> job_request = ds.jobs.generate_request(
457457
... app_id="matlab-r2023a",
458458
... input_dir_uri="tapis://designsafe.storage.default/username/input/",
459459
... script_filename="run_analysis.m",
@@ -506,8 +506,8 @@ def submit_request(self, job_request: Dict[str, Any]) -> SubmittedJob:
506506
JobSubmissionError: If the Tapis submission fails or encounters an error.
507507
508508
Example:
509-
>>> job_request = client.jobs.generate_request(...)
510-
>>> submitted_job = client.jobs.submit_request(job_request)
509+
>>> job_request = ds.jobs.generate_request(...)
510+
>>> submitted_job = ds.jobs.submit_request(job_request)
511511
>>> print(f"Job submitted with UUID: {submitted_job.uuid}")
512512
"""
513513
return jobs_module.submit_job_request(self._tapis, job_request)
@@ -523,7 +523,7 @@ def get(self, job_uuid: str) -> SubmittedJob:
523523
SubmittedJob: A SubmittedJob object for monitoring and managing the job.
524524
525525
Example:
526-
>>> job = client.jobs.get("12345678-1234-1234-1234-123456789abc")
526+
>>> job = ds.jobs.get("12345678-1234-1234-1234-123456789abc")
527527
>>> status = job.status
528528
"""
529529
return SubmittedJob(self._tapis, job_uuid)
@@ -541,7 +541,7 @@ def get_status(self, job_uuid: str) -> str:
541541
JobMonitorError: If status retrieval fails.
542542
543543
Example:
544-
>>> status = client.jobs.get_status("12345678-1234-1234-1234-123456789abc")
544+
>>> status = ds.jobs.get_status("12345678-1234-1234-1234-123456789abc")
545545
>>> print(f"Job status: {status}")
546546
"""
547547
return jobs_module.get_job_status(self._tapis, job_uuid)
@@ -555,7 +555,7 @@ def get_runtime_summary(self, job_uuid: str, verbose: bool = False):
555555
Defaults to False.
556556
557557
Example:
558-
>>> client.jobs.get_runtime_summary("12345678-1234-1234-1234-123456789abc")
558+
>>> ds.jobs.get_runtime_summary("12345678-1234-1234-1234-123456789abc")
559559
Runtime Summary
560560
---------------
561561
QUEUED time: 00:05:30
@@ -572,7 +572,44 @@ def interpret_status(self, final_status: str, job_uuid: Optional[str] = None):
572572
job_uuid (str, optional): The job UUID for context in the message.
573573
574574
Example:
575-
>>> client.jobs.interpret_status("FINISHED", "12345678-1234-1234-1234-123456789abc")
575+
>>> ds.jobs.interpret_status("FINISHED", "12345678-1234-1234-1234-123456789abc")
576576
Job 12345678-1234-1234-1234-123456789abc completed successfully.
577577
"""
578578
jobs_module.interpret_job_status(final_status, job_uuid)
579+
580+
def list(
581+
self,
582+
app_id: Optional[str] = None,
583+
status: Optional[str] = None,
584+
limit: int = 100,
585+
verbose: bool = False,
586+
):
587+
"""List jobs as a pandas DataFrame with optional filtering.
588+
589+
Fetches jobs from Tapis ordered by creation date (newest first)
590+
and returns them as a DataFrame. Filters are applied client-side.
591+
592+
Args:
593+
app_id (str, optional): Filter by application ID.
594+
status (str, optional): Filter by job status (e.g., "FINISHED").
595+
Case-insensitive.
596+
limit (int, optional): Maximum jobs to fetch. Defaults to 100.
597+
verbose (bool, optional): Print job count. Defaults to False.
598+
599+
Returns:
600+
pd.DataFrame: Job metadata with formatted datetime columns.
601+
602+
Raises:
603+
JobMonitorError: If the Tapis API call fails.
604+
605+
Example:
606+
>>> df = ds.jobs.list(app_id="matlab-r2023a", status="FINISHED")
607+
>>> print(df[["name", "uuid", "status", "created_dt"]])
608+
"""
609+
return jobs_module.list_jobs(
610+
self._tapis,
611+
app_id=app_id,
612+
status=status,
613+
limit=limit,
614+
verbose=verbose,
615+
)

dapi/jobs.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from tapipy.errors import BaseTapyException
1111
from dataclasses import dataclass, field, asdict
1212
from tqdm.auto import tqdm
13+
import pandas as pd
1314
from .apps import get_app_details
1415
from .exceptions import (
1516
JobSubmissionError,
@@ -1340,3 +1341,87 @@ def interpret_job_status(final_status: str, job_uuid: Optional[str] = None):
13401341
print(f"{job_id_str} ended with status: {final_status}")
13411342
else:
13421343
print(f"{job_id_str} ended with an unexpected status: {final_status}")
1344+
1345+
1346+
def list_jobs(
1347+
tapis_client: Tapis,
1348+
app_id: Optional[str] = None,
1349+
status: Optional[str] = None,
1350+
limit: int = 100,
1351+
verbose: bool = False,
1352+
) -> pd.DataFrame:
1353+
"""Fetch Tapis jobs and return them as a pandas DataFrame.
1354+
1355+
Retrieves jobs from Tapis ordered by creation date (newest first)
1356+
and optionally filters by app ID and/or status. Filters are applied
1357+
client-side after fetching.
1358+
1359+
Args:
1360+
tapis_client: Authenticated Tapis client instance.
1361+
app_id: Filter by application ID (e.g., "opensees-mp-s3").
1362+
status: Filter by job status (e.g., "FINISHED", "FAILED").
1363+
Case-insensitive.
1364+
limit: Maximum number of jobs to fetch from Tapis. Defaults to 100.
1365+
verbose: If True, prints the number of jobs found.
1366+
1367+
Returns:
1368+
DataFrame with job metadata and formatted datetime columns.
1369+
Priority columns appear first: name, uuid, status, appId, appVersion,
1370+
created_dt, ended_dt. Additional datetime columns include _dt
1371+
(timezone-aware) and _date (date only) variants for created, ended,
1372+
remoteStarted, and lastUpdated.
1373+
1374+
Raises:
1375+
JobMonitorError: If the Tapis API call fails.
1376+
1377+
Example:
1378+
>>> df = list_jobs(t, app_id="matlab-r2023a", status="FINISHED")
1379+
>>> print(df[["name", "uuid", "status", "created_dt"]])
1380+
"""
1381+
try:
1382+
jobs_list = tapis_client.jobs.getJobList(
1383+
limit=limit,
1384+
orderBy="created(desc)",
1385+
)
1386+
except BaseTapyException as e:
1387+
raise JobMonitorError(f"Failed to list jobs: {e}") from e
1388+
except Exception as e:
1389+
raise JobMonitorError(f"Unexpected error listing jobs: {e}") from e
1390+
1391+
if not jobs_list:
1392+
if verbose:
1393+
print("Found 0 jobs.")
1394+
return pd.DataFrame()
1395+
1396+
# Convert TapisResult objects to dicts
1397+
jobs_dicts = [job.__dict__ for job in jobs_list]
1398+
df = pd.DataFrame(jobs_dicts)
1399+
1400+
# Apply client-side filters
1401+
if app_id and "appId" in df.columns:
1402+
df = df[df["appId"] == app_id]
1403+
if status and "status" in df.columns:
1404+
df = df[df["status"] == status.upper()]
1405+
1406+
# Add formatted datetime columns
1407+
time_cols = ["created", "ended", "remoteStarted", "lastUpdated"]
1408+
for col in time_cols:
1409+
if col in df.columns:
1410+
df[f"{col}_dt"] = pd.to_datetime(df[col], utc=True, errors="coerce")
1411+
df[f"{col}_date"] = df[f"{col}_dt"].dt.date
1412+
1413+
# Reorder: priority columns first
1414+
priority = [
1415+
"name", "uuid", "status", "appId", "appVersion",
1416+
"created_dt", "ended_dt",
1417+
]
1418+
priority_present = [c for c in priority if c in df.columns]
1419+
remaining = [c for c in df.columns if c not in priority_present]
1420+
df = df[priority_present + remaining]
1421+
1422+
df = df.reset_index(drop=True)
1423+
1424+
if verbose:
1425+
print(f"Found {len(df)} jobs.")
1426+
1427+
return df

docs/api/index.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ The DAPI package is organized into several core modules:
2929
from dapi import DSClient
3030

3131
# Initialize client
32-
client = DSClient()
32+
ds = DSClient()
3333

3434
# Access different services
35-
client.jobs.generate_request(...)
36-
client.files.upload(...)
37-
client.db.ngl.read_sql(...)
35+
ds.jobs.generate_request(...)
36+
ds.files.upload(...)
37+
ds.db.ngl.read_sql(...)
3838
```
3939

4040
### **Common Operations**
41-
- **Submit Jobs**: `client.jobs.submit_request(job_dict)`
41+
- **Submit Jobs**: `ds.jobs.submit_request(job_dict)`
4242
- **Monitor Jobs**: `submitted_job.monitor()`
43-
- **File Upload**: `client.files.upload(local_path, remote_uri)`
44-
- **File Download**: `client.files.download(remote_uri, local_path)`
45-
- **Database Query**: `client.db.ngl.read_sql("SELECT * FROM table")`
43+
- **File Upload**: `ds.files.upload(local_path, remote_uri)`
44+
- **File Download**: `ds.files.download(remote_uri, local_path)`
45+
- **Database Query**: `ds.db.ngl.read_sql("SELECT * FROM table")`
4646

4747
### **Advanced Features**
4848
- **Archive Management**: Custom job result organization

docs/api/jobs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ Job submission, monitoring, and management functionality for DesignSafe computat
2828
.. autofunction:: dapi.jobs.interpret_job_status
2929
```
3030

31+
## Listing Jobs
32+
33+
```{eval-rst}
34+
.. autofunction:: dapi.jobs.list_jobs
35+
```
36+
3137
## SubmittedJob Class
3238

3339
```{eval-rst}

0 commit comments

Comments
 (0)