diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..7b99365 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: Build Documentation + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + pip install -e . + + - name: Build documentation + run: | + cd docs + make html + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..bb7a525 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,18 @@ +# Minimal makefile for Sphinx documentation + +# You can set these variables from the command line. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api/aws.md b/docs/api/aws.md new file mode 100644 index 0000000..2513f33 --- /dev/null +++ b/docs/api/aws.md @@ -0,0 +1,36 @@ +# AWS Credentials + +```{eval-rst} +.. automodule:: maap.AWS + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: maap.AWS.AWS + :members: + :special-members: __init__ +``` + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# Get requester-pays credentials +creds = maap.aws.requester_pays_credentials() +print(f"Access Key: {creds['accessKeyId']}") + +# Generate a signed URL +signed = maap.aws.s3_signed_url('bucket', 'path/to/file.h5') +print(f"Signed URL: {signed['url']}") + +# Get Earthdata DAAC credentials +daac_creds = maap.aws.earthdata_s3_credentials( + 'https://data.lpdaac.earthdatacloud.nasa.gov/s3credentials' +) + +# Get workspace bucket credentials +workspace_creds = maap.aws.workspace_bucket_credentials() +``` diff --git a/docs/api/config.md b/docs/api/config.md new file mode 100644 index 0000000..bc39ed1 --- /dev/null +++ b/docs/api/config.md @@ -0,0 +1,42 @@ +# Configuration + +```{eval-rst} +.. automodule:: maap.config_reader + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: maap.config_reader.MaapConfig + :members: + :special-members: __init__ +``` + +## Environment Variables + +The following environment variables can be used to configure the MAAP client: + +| Variable | Description | +|----------|-------------| +| `MAAP_API_HOST` | Override the default MAAP API hostname | +| `MAAP_API_HOST_SCHEME` | URL scheme (http/https) for API connections | +| `MAAP_CMR_PAGE_SIZE` | Number of results per page for CMR queries | +| `MAAP_CMR_CONTENT_TYPE` | Content type for CMR requests | +| `MAAP_PGT` | Proxy granting ticket for authentication | +| `MAAP_AWS_ACCESS_KEY_ID` | AWS access key for S3 operations | +| `MAAP_AWS_SECRET_ACCESS_KEY` | AWS secret key for S3 operations | +| `MAAP_S3_USER_UPLOAD_BUCKET` | S3 bucket for user file uploads | +| `MAAP_S3_USER_UPLOAD_DIR` | S3 directory for user file uploads | +| `MAAP_MAPBOX_ACCESS_TOKEN` | Mapbox token for visualization | + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# Access configuration +print(f"API Root: {maap.config.maap_api_root}") +print(f"Page Size: {maap.config.page_size}") +print(f"Granule Search URL: {maap.config.search_granule_url}") +``` diff --git a/docs/api/dps.md b/docs/api/dps.md new file mode 100644 index 0000000..3a30b6e --- /dev/null +++ b/docs/api/dps.md @@ -0,0 +1,57 @@ +# DPS (Data Processing System) + +The DPS module provides classes for submitting and managing processing jobs. + +## DPSJob + +```{eval-rst} +.. automodule:: maap.dps.dps_job + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: maap.dps.dps_job.DPSJob + :members: + :special-members: __init__ +``` + +## DpsHelper + +```{eval-rst} +.. automodule:: maap.dps.DpsHelper + :members: + :undoc-members: + :show-inheritance: +``` + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# Submit a job +job = maap.submitJob( + identifier='my_analysis', + algo_id='my_algorithm', + version='main', + queue='maap-dps-worker-8gb', + input_file='s3://bucket/input.tif' +) + +# Check status +print(f"Job ID: {job.id}") +print(f"Status: {job.status}") + +# Wait for completion +job.wait_for_completion() + +# Get results +if job.status == 'Succeeded': + print(f"Outputs: {job.outputs}") + print(f"Duration: {job.job_duration_seconds}s") + +# Cancel a job +job.cancel_job() +``` diff --git a/docs/api/maap.md b/docs/api/maap.md new file mode 100644 index 0000000..5b06c20 --- /dev/null +++ b/docs/api/maap.md @@ -0,0 +1,34 @@ +# MAAP Client + +```{eval-rst} +.. automodule:: maap.maap + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ +``` + +The {class}`~maap.maap.MAAP` class is the main entry point for all MAAP operations. + +## Example Usage + +```python +from maap.maap import MAAP + +# Initialize +maap = MAAP() + +# Search granules +granules = maap.searchGranule(short_name='GEDI02_A', limit=10) + +# Search collections +collections = maap.searchCollection(provider='MAAP') + +# Submit a job +job = maap.submitJob( + identifier='analysis', + algo_id='my_algo', + version='main', + queue='maap-dps-worker-8gb' +) +``` diff --git a/docs/api/profile.md b/docs/api/profile.md new file mode 100644 index 0000000..4ad25b5 --- /dev/null +++ b/docs/api/profile.md @@ -0,0 +1,26 @@ +# User Profile + +```{eval-rst} +.. automodule:: maap.Profile + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: maap.Profile.Profile + :members: + :special-members: __init__ +``` + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# Get user account information +info = maap.profile.account_info() +if info: + print(f"User ID: {info['id']}") + print(f"Username: {info['username']}") +``` diff --git a/docs/api/result.md b/docs/api/result.md new file mode 100644 index 0000000..9cbafa3 --- /dev/null +++ b/docs/api/result.md @@ -0,0 +1,64 @@ +# Result Classes + +```{eval-rst} +.. automodule:: maap.Result + :members: + :undoc-members: + :show-inheritance: +``` + +The Result module provides classes for handling CMR search results. + +## Classes + +### Result + +Base class for CMR results. + +```{eval-rst} +.. autoclass:: maap.Result.Result + :members: + :special-members: __init__ +``` + +### Granule + +Represents a CMR granule (individual data file). + +```{eval-rst} +.. autoclass:: maap.Result.Granule + :members: + :special-members: __init__ +``` + +### Collection + +Represents a CMR collection (dataset). + +```{eval-rst} +.. autoclass:: maap.Result.Collection + :members: + :special-members: __init__ +``` + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# Search granules +granules = maap.searchGranule(short_name='GEDI02_A', limit=5) + +for granule in granules: + # Get URLs + s3_url = granule.getS3Url() + http_url = granule.getHttpUrl() + + # Download + local_path = granule.getData(destpath='/tmp') + + # Get description + print(granule.getDescription()) +``` diff --git a/docs/api/secrets.md b/docs/api/secrets.md new file mode 100644 index 0000000..16adedb --- /dev/null +++ b/docs/api/secrets.md @@ -0,0 +1,35 @@ +# User Secrets + +```{eval-rst} +.. automodule:: maap.Secrets + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: maap.Secrets.Secrets + :members: + :special-members: __init__ +``` + +## Example Usage + +```python +from maap.maap import MAAP + +maap = MAAP() + +# List all secrets +secrets_list = maap.secrets.get_secrets() +for secret in secrets_list: + print(f"Secret: {secret['secret_name']}") + +# Get a specific secret +value = maap.secrets.get_secret('my_api_key') +print(f"Value: {value}") + +# Add a new secret +maap.secrets.add_secret('my_new_key', 'my_secret_value') + +# Delete a secret +maap.secrets.delete_secret('my_old_key') +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..a193ae8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,119 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import sys + +# Add the project root to the path for autodoc +sys.path.insert(0, os.path.abspath('..')) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'maap-py' +copyright = '2024, NASA MAAP Project / Jet Propulsion Laboratory' +author = 'MAAP Development Team' +release = '4.2.0' +version = '4.2.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'myst_parser', +] + +# MyST parser configuration +myst_enable_extensions = [ + "colon_fence", + "deflist", + "fieldlist", + "html_admonition", + "html_image", + "replacements", + "smartquotes", + "strikethrough", + "substitution", + "tasklist", +] + +# Source file suffixes +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +# Napoleon settings for NumPy-style docstrings +napoleon_google_docstring = False +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_notes = True +napoleon_use_admonition_for_references = True +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_use_keyword = True +napoleon_type_aliases = None + +# Autodoc settings +autodoc_default_options = { + 'members': True, + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': True, + 'exclude-members': '__weakref__', + 'show-inheritance': True, +} + +autodoc_typehints = 'description' +autodoc_typehints_description_target = 'documented' + +# Autosummary settings +autosummary_generate = True + +# Intersphinx mapping +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'requests': ('https://requests.readthedocs.io/en/latest/', None), + 'boto3': ('https://boto3.amazonaws.com/v1/documentation/api/latest/', None), +} + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# Theme options +html_theme_options = { + 'navigation_depth': 4, + 'collapse_navigation': False, + 'sticky_navigation': True, + 'includehidden': True, + 'titles_only': False, +} + +# Additional HTML settings +html_show_sourcelink = True +html_show_sphinx = True +html_show_copyright = True + +# -- Options for LaTeX output ------------------------------------------------ +latex_elements = { + 'papersize': 'letterpaper', + 'pointsize': '10pt', +} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c1bc030 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,78 @@ +# maap-py Documentation + +**maap-py** is the official Python client library for the NASA MAAP +(Multi-Mission Algorithm and Analysis Platform) API. + +The library provides a simple and intuitive interface for: + +- **Data Discovery**: Search for granules and collections in the CMR (Common Metadata Repository) +- **Data Access**: Download data files from S3 or HTTP endpoints +- **Algorithm Management**: Register, build, and manage algorithms on the DPS +- **Job Execution**: Submit and monitor processing jobs +- **User Management**: Access user profile, secrets, and AWS credentials + +## Quick Start + +### Installation + +Install maap-py using pip: + +```bash +pip install maap-py +``` + +### Basic Usage + +```python +from maap.maap import MAAP + +# Initialize the client +maap = MAAP() + +# Search for granules +granules = maap.searchGranule( + short_name='GEDI02_A', + bounding_box='-122.5,37.5,-121.5,38.5', + limit=10 +) + +# Download data +for granule in granules: + local_path = granule.getData(destpath='/tmp') + print(f"Downloaded: {local_path}") + +# Submit a job +job = maap.submitJob( + identifier='my_analysis', + algo_id='my_algorithm', + version='main', + queue='maap-dps-worker-8gb', + input_file='s3://bucket/input.tif' +) + +# Wait for completion +job.wait_for_completion() +print(f"Status: {job.status}") +print(f"Outputs: {job.outputs}") +``` + +## Contents + +```{toctree} +:maxdepth: 2 +:caption: API Reference + +api/maap +api/result +api/dps +api/aws +api/profile +api/secrets +api/config +``` + +## Indices and tables + +- {ref}`genindex` +- {ref}`modindex` +- {ref}`search` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..60f5e99 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=7.0 +sphinx-rtd-theme>=2.0 +myst-parser>=2.0 diff --git a/maap/AWS.py b/maap/AWS.py index 48b10e7..3803c84 100644 --- a/maap/AWS.py +++ b/maap/AWS.py @@ -1,3 +1,38 @@ +""" +AWS Credential Management +========================= + +This module provides the :class:`AWS` class for obtaining AWS credentials +and signed URLs for accessing S3 data. + +The AWS class supports multiple credential scenarios: + +* Requester-pays bucket access (for public datasets requiring authentication) +* Pre-signed URL generation for temporary access +* Earthdata S3 credentials for accessing external DAACs +* Workspace bucket credentials for MAAP-managed storage + +Example +------- +Get credentials for S3 access:: + + from maap.maap import MAAP + + maap = MAAP() + + # Get requester-pays credentials + creds = maap.aws.requester_pays_credentials() + print(f"Access Key: {creds['accessKeyId']}") + + # Generate a signed URL + signed = maap.aws.s3_signed_url('bucket-name', 'path/to/file.h5') + print(f"Signed URL: {signed['url']}") + +See Also +-------- +:class:`maap.maap.MAAP` : Main client class +""" + import json import logging import urllib @@ -6,7 +41,41 @@ class AWS: """ - Functions used for Member API interfacing + Interface for AWS credential operations. + + The AWS class provides methods to obtain temporary AWS credentials and + pre-signed URLs for accessing S3 data from various sources. + + Parameters + ---------- + requester_pays_endpoint : str + URL endpoint for requester-pays credentials. + s3_signed_url_endpoint : str + URL endpoint for generating signed URLs. + earthdata_s3_credentials_endpoint : str + URL endpoint for Earthdata S3 credentials. + workspace_bucket_endpoint : str + URL endpoint for workspace bucket credentials. + api_header : dict + HTTP headers including authentication tokens. + + Examples + -------- + Access via MAAP client:: + + >>> maap = MAAP() + >>> creds = maap.aws.requester_pays_credentials() + >>> print(f"Key: {creds['accessKeyId']}") + + Notes + ----- + The AWS instance is automatically created when initializing the + :class:`~maap.maap.MAAP` client and is accessible via ``maap.aws``. + + See Also + -------- + :class:`~maap.Profile.Profile` : User profile management + :class:`~maap.Secrets.Secrets` : User secrets management """ def __init__( @@ -25,6 +94,55 @@ def __init__( self._logger = logging.getLogger(__name__) def requester_pays_credentials(self, expiration=60 * 60 * 12): + """ + Get temporary credentials for requester-pays S3 buckets. + + Obtains AWS credentials that can be used to access S3 buckets + configured with requester-pays enabled. + + Parameters + ---------- + expiration : int, optional + Credential validity duration in seconds. Default is 43200 + (12 hours). + + Returns + ------- + dict + Dictionary containing AWS credentials: + + - ``accessKeyId``: AWS access key ID + - ``secretAccessKey``: AWS secret access key + - ``sessionToken``: Temporary session token + - ``expiration``: Token expiration timestamp + + Raises + ------ + requests.HTTPError + If the credential request fails. + + Examples + -------- + Get credentials and configure boto3:: + + >>> creds = maap.aws.requester_pays_credentials() + >>> import boto3 + >>> s3 = boto3.client( + ... 's3', + ... aws_access_key_id=creds['accessKeyId'], + ... aws_secret_access_key=creds['secretAccessKey'], + ... aws_session_token=creds['sessionToken'] + ... ) + + Get short-lived credentials:: + + >>> creds = maap.aws.requester_pays_credentials(expiration=3600) + + See Also + -------- + :meth:`earthdata_s3_credentials` : For external DAAC access + :meth:`workspace_bucket_credentials` : For MAAP workspace access + """ headers = self._api_header headers["Accept"] = "application/json" @@ -37,6 +155,61 @@ def requester_pays_credentials(self, expiration=60 * 60 * 12): return json.loads(response.text) def s3_signed_url(self, bucket, key, expiration=60 * 60 * 12): + """ + Generate a pre-signed URL for an S3 object. + + Creates a temporary URL that allows access to a private S3 object + without requiring AWS credentials. + + Parameters + ---------- + bucket : str + S3 bucket name. + key : str + S3 object key (path within the bucket). + expiration : int, optional + URL validity duration in seconds. Default is 43200 (12 hours). + + Returns + ------- + dict + Dictionary containing: + + - ``url``: Pre-signed URL for the object + - Additional metadata about the signed URL + + Raises + ------ + requests.HTTPError + If the URL generation fails. + + Examples + -------- + Generate a signed URL:: + + >>> result = maap.aws.s3_signed_url( + ... 'maap-data-store', + ... 'path/to/data.h5' + ... ) + >>> print(f"Access data at: {result['url']}") + + Short-lived URL for sharing:: + + >>> result = maap.aws.s3_signed_url( + ... 'bucket', 'key', + ... expiration=3600 # 1 hour + ... ) + + Notes + ----- + Pre-signed URLs allow sharing access to private S3 objects with + users who don't have AWS credentials. The URL contains temporary + authentication parameters. + + See Also + -------- + :meth:`requester_pays_credentials` : Get full credentials instead + """ headers = self._api_header headers["Accept"] = "application/json" _url = self._s3_signed_url_endpoint.replace("{bucket}", bucket).replace( @@ -51,6 +224,59 @@ def s3_signed_url(self, bucket, key, expiration=60 * 60 * 12): return json.loads(response.text) def earthdata_s3_credentials(self, endpoint_uri): + """ + Get S3 credentials for accessing external Earthdata DAACs. + + Obtains temporary AWS credentials for accessing data stored in + external DAAC S3 buckets through Earthdata OAuth. + + Parameters + ---------- + endpoint_uri : str + The S3 credential endpoint URL for the target DAAC. + Each DAAC has a unique endpoint. + + Returns + ------- + dict + Dictionary containing AWS credentials and DAAC information: + + - ``accessKeyId``: AWS access key ID + - ``secretAccessKey``: AWS secret access key + - ``sessionToken``: Temporary session token + - ``expiration``: Token expiration timestamp + - ``DAAC``: The DAAC hostname (extracted from endpoint) + + Raises + ------ + requests.HTTPError + If the credential request fails. + + Examples + -------- + Get LP DAAC credentials:: + + >>> creds = maap.aws.earthdata_s3_credentials( + ... 'https://data.lpdaac.earthdatacloud.nasa.gov/s3credentials' + ... ) + >>> print(f"DAAC: {creds['DAAC']}") + >>> # Use credentials to access LP DAAC data + + Notes + ----- + Different DAACs have different S3 credential endpoints. Check the + DAAC's documentation for the correct endpoint URL. + + Common DAAC endpoints: + + - LP DAAC: ``https://data.lpdaac.earthdatacloud.nasa.gov/s3credentials`` + - NSIDC: ``https://data.nsidc.earthdatacloud.nasa.gov/s3credentials`` + - GES DISC: ``https://data.gesdisc.earthdata.nasa.gov/s3credentials`` + + See Also + -------- + :meth:`requester_pays_credentials` : For MAAP-hosted data + """ headers = self._api_header headers["Accept"] = "application/json" _parsed_endpoint = urllib.parse.quote(urllib.parse.quote(endpoint_uri, safe="")) @@ -67,6 +293,51 @@ def earthdata_s3_credentials(self, endpoint_uri): return result def workspace_bucket_credentials(self): + """ + Get credentials for the MAAP workspace bucket. + + Obtains temporary AWS credentials for accessing the user's + workspace storage on MAAP. + + Returns + ------- + dict + Dictionary containing AWS credentials: + + - ``accessKeyId``: AWS access key ID + - ``secretAccessKey``: AWS secret access key + - ``sessionToken``: Temporary session token + - ``expiration``: Token expiration timestamp + - Additional workspace bucket information + + Raises + ------ + requests.HTTPError + If the credential request fails. + + Examples + -------- + Access workspace bucket:: + + >>> creds = maap.aws.workspace_bucket_credentials() + >>> import boto3 + >>> s3 = boto3.client( + ... 's3', + ... aws_access_key_id=creds['accessKeyId'], + ... aws_secret_access_key=creds['secretAccessKey'], + ... aws_session_token=creds['sessionToken'] + ... ) + + Notes + ----- + The workspace bucket is user-specific storage provided by MAAP + for storing analysis results and intermediate files. + + See Also + -------- + :meth:`requester_pays_credentials` : For accessing external data + :meth:`maap.maap.MAAP.uploadFiles` : Upload files to shared storage + """ headers = self._api_header headers["Accept"] = "application/json" diff --git a/maap/Profile.py b/maap/Profile.py index 1a72042..3bc2ce5 100644 --- a/maap/Profile.py +++ b/maap/Profile.py @@ -1,3 +1,25 @@ +""" +User Profile Management +======================= + +This module provides the :class:`Profile` class for accessing user account +information from the MAAP Member API. + +Example +------- +Access user profile information:: + + from maap.maap import MAAP + + maap = MAAP() + profile = maap.profile.account_info() + print(f"Username: {profile['username']}") + +See Also +-------- +:class:`maap.maap.MAAP` : Main client class +""" + import requests import logging import json @@ -5,14 +27,86 @@ class Profile: """ - Functions used for Member API interfacing + Interface for user profile operations. + + The Profile class provides methods to retrieve user account information + from the MAAP Member API. + + Parameters + ---------- + profile_endpoint : str + URL endpoint for the Member API. + api_header : dict + HTTP headers including authentication tokens. + + Examples + -------- + Access via MAAP client:: + + >>> maap = MAAP() + >>> info = maap.profile.account_info() + >>> print(f"User ID: {info['id']}") + >>> print(f"Username: {info['username']}") + + Notes + ----- + The Profile instance is automatically created when initializing the + :class:`~maap.maap.MAAP` client and is accessible via ``maap.profile``. + + See Also + -------- + :class:`~maap.AWS.AWS` : AWS credential management + :class:`~maap.Secrets.Secrets` : User secrets management """ + def __init__(self, profile_endpoint, api_header): self._api_header = api_header self._profile_endpoint = profile_endpoint self._logger = logging.getLogger(__name__) - def account_info(self, proxy_ticket = None): + def account_info(self, proxy_ticket=None): + """ + Get user account information. + + Retrieves the profile information for the currently authenticated user. + + Parameters + ---------- + proxy_ticket : str, optional + Proxy granting ticket for authentication. If not provided, uses + the ticket from environment variables if available. + + Returns + ------- + dict or None + Dictionary containing user profile information, or ``None`` if + the request fails. Profile fields include: + + - ``id``: User's unique identifier + - ``username``: User's username + - Additional profile fields as configured by the MAAP platform + + Examples + -------- + Get basic account info:: + + >>> info = maap.profile.account_info() + >>> if info: + ... print(f"Username: {info['username']}") + + Use with explicit proxy ticket:: + + >>> info = maap.profile.account_info(proxy_ticket='PGT-...') + + Notes + ----- + This method is used internally by :meth:`~maap.maap.MAAP.submitJob` + to automatically include the username with job submissions. + + See Also + -------- + :class:`~maap.Secrets.Secrets` : Manage user secrets + """ headers = self._api_header headers['Accept'] = 'application/json' if 'proxy-ticket' not in headers and proxy_ticket: @@ -27,7 +121,3 @@ def account_info(self, proxy_ticket = None): return json.loads(response.text) else: return None - - - - diff --git a/maap/Result.py b/maap/Result.py index 012ea53..2f96012 100644 --- a/maap/Result.py +++ b/maap/Result.py @@ -1,3 +1,42 @@ +""" +Result Classes for CMR Search Results +====================================== + +This module provides classes for handling CMR (Common Metadata Repository) +search results, including granules and collections. + +Classes +------- +Result + Base class for CMR search results with data download capabilities. +Granule + Represents a CMR granule (individual data file) with multiple access URLs. +Collection + Represents a CMR collection (dataset) with metadata access. + +Example +------- +Search and download granules:: + + from maap.maap import MAAP + + maap = MAAP() + granules = maap.searchGranule(short_name='GEDI02_A', limit=5) + + for granule in granules: + # Get download URLs + s3_url = granule.getS3Url() + http_url = granule.getHttpUrl() + + # Download to local filesystem + local_path = granule.getData(destpath='/tmp') + print(f"Downloaded: {local_path}") + +See Also +-------- +:class:`maap.maap.MAAP` : Main client class for searching +""" + import json import os import shutil @@ -15,17 +54,89 @@ class Result(dict): - """Class to structure the response XML from a CMR API request.""" + """ + Base class for CMR search result items. + + The Result class extends Python's dict to provide convenient access to + CMR metadata fields while adding methods for data download. + + This class serves as the base for :class:`Granule` and :class:`Collection` + classes, which provide type-specific functionality. + + Attributes + ---------- + _location : str or None + Primary download URL (typically S3). + _fallback : str or None + Fallback download URL (typically HTTPS). + _downloadname : str or None + Filename to use when downloading. + + Notes + ----- + Result objects behave like dictionaries, allowing direct access to CMR + metadata fields using bracket notation (e.g., ``result['Granule']['GranuleUR']``). + + See Also + -------- + :class:`Granule` : Granule-specific result class + :class:`Collection` : Collection-specific result class + """ _location = None _fallback = None def getData(self, destpath=".", overwrite=False): """ - Download the dataset into file system - :param destpath: use the current directory as default - :param overwrite: don't download by default if the target file exists - :return: + Download the data file to the local filesystem. + + Downloads the data file associated with this result to a local + directory. Supports S3, HTTP/HTTPS, and FTP protocols. + + Parameters + ---------- + destpath : str, optional + Destination directory for the download. Default is the current + working directory (``'.'``). + overwrite : bool, optional + If ``True``, overwrite existing files. If ``False`` (default), + skip download if file already exists. + + Returns + ------- + str or None + Local path to the downloaded file, or ``None`` if no download + URL is available. + + Examples + -------- + Download to current directory:: + + >>> local_path = granule.getData() + >>> print(f"Downloaded to: {local_path}") + + Download to specific directory:: + + >>> local_path = granule.getData(destpath='/tmp/data') + + Force re-download:: + + >>> local_path = granule.getData(overwrite=True) + + Notes + ----- + Download strategy: + + 1. If URL is FTP, downloads directly via FTP + 2. If URL is S3, attempts direct S3 download using boto3 + 3. If S3 fails, falls back to HTTPS URL if available + 4. Otherwise uses HTTP download with authentication + + See Also + -------- + :meth:`getDownloadUrl` : Get the download URL without downloading + :meth:`getS3Url` : Get the S3 URL + :meth:`getHttpUrl` : Get the HTTP URL """ url = self._location destfile = self._downloadname.replace("/", "") @@ -63,28 +174,82 @@ def getData(self, destpath=".", overwrite=False): def getLocalPath(self, destpath=".", overwrite=False): """ - Deprecated method. Use getData() instead. + Download data to local filesystem. + + .. deprecated:: + Use :meth:`getData` instead. This method is kept for backwards + compatibility. + + Parameters + ---------- + destpath : str, optional + Destination directory. + overwrite : bool, optional + Whether to overwrite existing files. + + Returns + ------- + str or None + Local path to the downloaded file. + + See Also + -------- + :meth:`getData` : Preferred method for downloading data """ return self.getData(destpath, overwrite) def _convertS3toHttp(self, url): + """ + Convert an S3 URL to an HTTPS URL. + + Parameters + ---------- + url : str + S3 URL in the format ``s3://bucket/key``. + + Returns + ------- + str + HTTPS URL pointing to the same object. + """ url = url[5:].split("/") url[0] += ".s3.amazonaws.com" url = "https://" + "/".join(url) return url - # When retrieving granule data, always try an unauthenticated HTTPS request first, - # then fall back to EDL federated login. - # - # In the case where an external DAAC is called (which we know from the `cmr_host` - # parameter), we may consider skipping the unauthenticated HTTPS request, but this - # method assumes that granules can both be publicly accessible or EDL-restricted. - # In the former case, this conditional logic will stream the data directly from CMR, - # rather than via the MAAP API proxy. - # - # This direct interface with CMR is the default method since it reduces traffic to - # the MAAP API. def _getHttpData(self, url, overwrite, dest): + """ + Download data via HTTP with authentication fallback. + + Downloads data from an HTTP URL, automatically handling authentication + when required. First attempts an unauthenticated request, then falls + back to EDL (Earthdata Login) authentication if needed. + + Parameters + ---------- + url : str + The HTTP URL to download from. + overwrite : bool + Whether to overwrite existing files. + dest : str + Local destination path for the downloaded file. + + Returns + ------- + str + The local path to the downloaded file. + + Notes + ----- + Authentication strategy: + + - First attempts unauthenticated request (for public data) + - If 401 response and running in DPS, uses machine token + - If 401 response and running in ADE, uses MAAP API proxy + + This direct interface with CMR is preferred as it reduces traffic + to the MAAP API. + """ if overwrite or not os.path.exists(dest): r = requests.get(url, stream=True) @@ -136,29 +301,111 @@ def _getHttpData(self, url, overwrite, dest): def getDownloadUrl(self, s3=True): """ - Get granule download url - :param s3: True returns the s3 url, False the http url - :return: + Get the download URL for this result. + + Parameters + ---------- + s3 : bool, optional + If ``True`` (default), return the S3 URL. If ``False``, return + the HTTP URL. + + Returns + ------- + str or None + The download URL, or ``None`` if not available. + + Examples + -------- + Get S3 URL (default):: + + >>> url = granule.getDownloadUrl() + >>> print(url) + s3://bucket/path/file.h5 + + Get HTTP URL:: + + >>> url = granule.getDownloadUrl(s3=False) + >>> print(url) + https://data.maap-project.org/path/file.h5 + + See Also + -------- + :meth:`getS3Url` : Get S3 URL directly + :meth:`getHttpUrl` : Get HTTP URL directly + :meth:`getData` : Download the file """ return self.getS3Url() if s3 else self.getHttpUrl() def getHttpUrl(self): """ - Get granule http url - :return: + Get the HTTP download URL for this result. + + Returns + ------- + str or None + The HTTPS URL for downloading, or ``None`` if not available. + + Examples + -------- + :: + + >>> http_url = granule.getHttpUrl() + >>> print(http_url) + https://data.maap-project.org/path/file.h5 + + See Also + -------- + :meth:`getS3Url` : Get S3 URL + :meth:`getDownloadUrl` : Get either URL type """ return self._fallback def getS3Url(self): """ - Get granule s3 url - :return: + Get the S3 download URL for this result. + + Returns + ------- + str or None + The S3 URL for downloading, or ``None`` if not available. + + Examples + -------- + :: + + >>> s3_url = granule.getS3Url() + >>> print(s3_url) + s3://maap-data-store/path/file.h5 + + Notes + ----- + S3 URLs require appropriate AWS credentials to access. When running + in the MAAP ADE or DPS, credentials are typically configured + automatically. + + See Also + -------- + :meth:`getHttpUrl` : Get HTTP URL + :meth:`getDownloadUrl` : Get either URL type """ return self._location def getDescription(self): """ - :return: + Get a human-readable description of this result. + + Returns + ------- + str + A formatted string containing the granule identifier, last update + time, and collection concept ID. + + Examples + -------- + :: + + >>> print(granule.getDescription()) + GEDI02_A_2019123_O02389_T05321_02_001_01.h5 Updated 2020-01-15 (C1234567890-MAAP) """ return "{} Updated {} ({})".format( self["Granule"]["GranuleUR"].ljust(70), @@ -168,6 +415,52 @@ def getDescription(self): class Collection(Result): + """ + CMR Collection search result. + + Represents a collection (dataset) from the CMR. Collections contain + metadata about a group of related data files (granules). + + Parameters + ---------- + metaResult : dict + The CMR metadata dictionary for this collection. + maap_host : str + The MAAP API hostname. + + Attributes + ---------- + _location : str + URL to the UMM-JSON metadata for this collection. + _downloadname : str + The collection short name. + + Examples + -------- + Search for collections:: + + >>> collections = maap.searchCollection(short_name='GEDI02_A') + >>> for c in collections: + ... print(c['Collection']['ShortName']) + ... print(c['Collection']['Description']) + + Access collection metadata:: + + >>> collection = collections[0] + >>> print(collection['concept-id']) + >>> print(collection['Collection']['ShortName']) + + Notes + ----- + Collection objects inherit dictionary access from :class:`Result`, + allowing direct access to CMR metadata fields. + + See Also + -------- + :class:`Granule` : Individual data file results + :meth:`maap.maap.MAAP.searchCollection` : Search for collections + """ + def __init__(self, metaResult, maap_host): for k in metaResult: self[k] = metaResult[k] @@ -179,6 +472,77 @@ def __init__(self, metaResult, maap_host): class Granule(Result): + """ + CMR Granule search result. + + Represents a granule (individual data file) from the CMR. Granules are + the actual data products that can be downloaded and analyzed. + + Parameters + ---------- + metaResult : dict + The CMR metadata dictionary for this granule. + awsAccessKey : str + AWS access key for S3 operations. + awsAccessSecret : str + AWS secret key for S3 operations. + cmrFileUrl : str + Base CMR file URL for authenticated downloads. + apiHeader : dict + HTTP headers for API requests. + dps : DpsHelper + DPS helper for token management. + + Attributes + ---------- + _location : str or None + Primary download URL (preferably S3). + _fallback : str or None + Fallback HTTPS download URL. + _downloadname : str or None + Filename for downloads. + _OPeNDAPUrl : str or None + OPeNDAP data access URL if available. + _BrowseUrl : str or None + Browse image URL if available. + _relatedUrls : list or None + All available access URLs. + + Examples + -------- + Search and access granule metadata:: + + >>> granules = maap.searchGranule(short_name='GEDI02_A', limit=5) + >>> granule = granules[0] + >>> print(granule['Granule']['GranuleUR']) + + Download granule data:: + + >>> # Get URLs + >>> s3_url = granule.getS3Url() + >>> http_url = granule.getHttpUrl() + >>> + >>> # Download to local filesystem + >>> local_path = granule.getData(destpath='/tmp') + + Access OPeNDAP URL:: + + >>> opendap_url = granule.getOPeNDAPUrl() + >>> if opendap_url: + ... print(f"OPeNDAP access: {opendap_url}") + + Notes + ----- + Granule objects attempt to find both S3 and HTTPS URLs from the + available OnlineAccessURLs. S3 URLs are preferred for performance + when running within AWS. + + See Also + -------- + :class:`Collection` : Dataset metadata results + :meth:`maap.maap.MAAP.searchGranule` : Search for granules + """ + def __init__( self, metaResult, awsAccessKey, awsAccessSecret, cmrFileUrl, apiHeader, dps ): @@ -203,7 +567,6 @@ def __init__( for k in metaResult: self[k] = metaResult[k] - # TODO: make self._location an array and consolidate with _relatedUrls try: self._relatedUrls = self["Granule"]["OnlineAccessURLs"]["OnlineAccessURL"] @@ -254,4 +617,37 @@ def __init__( pass def getOPeNDAPUrl(self): + """ + Get the OPeNDAP data access URL for this granule. + + OPeNDAP (Open-source Project for a Network Data Access Protocol) + provides a way to access remote data subsets without downloading + entire files. + + Returns + ------- + str or None + The OPeNDAP URL if available, or ``None`` if the granule + does not support OPeNDAP access. + + Examples + -------- + :: + + >>> opendap_url = granule.getOPeNDAPUrl() + >>> if opendap_url: + ... # Use xarray or other tools to access data + ... import xarray as xr + ... ds = xr.open_dataset(opendap_url) + + Notes + ----- + Not all granules have OPeNDAP URLs. This depends on whether the + data provider has enabled OPeNDAP access for the dataset. + + See Also + -------- + :meth:`getS3Url` : Get S3 download URL + :meth:`getHttpUrl` : Get HTTP download URL + """ return self._OPeNDAPUrl diff --git a/maap/__init__.py b/maap/__init__.py index e69de29..a436ba3 100644 --- a/maap/__init__.py +++ b/maap/__init__.py @@ -0,0 +1,109 @@ +""" +MAAP Python Client Library +========================== + +**maap-py** is the official Python client for the NASA MAAP (Multi-Mission +Algorithm and Analysis Platform) API. + +The library provides a comprehensive interface for: + +* **Data Discovery**: Search for granules and collections in CMR +* **Data Access**: Download data files from S3 or HTTP endpoints +* **Algorithm Management**: Register, build, and manage algorithms +* **Job Execution**: Submit and monitor processing jobs on the DPS +* **User Management**: Access profile, secrets, and AWS credentials + +Quick Start +----------- + +Basic usage:: + + from maap.maap import MAAP + + # Initialize the client + maap = MAAP() + + # Search for granules + granules = maap.searchGranule( + short_name='GEDI02_A', + limit=10 + ) + + # Download data + for granule in granules: + local_path = granule.getData(destpath='/tmp') + + # Submit a job + job = maap.submitJob( + identifier='my_job', + algo_id='my_algorithm', + version='main', + queue='maap-dps-worker-8gb' + ) + + # Wait for completion + job.wait_for_completion() + +Main Classes +------------ + +:class:`~maap.maap.MAAP` + Main client class for all MAAP operations. + +:class:`~maap.Result.Granule` + Represents a CMR granule (data file). + +:class:`~maap.Result.Collection` + Represents a CMR collection (dataset). + +:class:`~maap.dps.dps_job.DPSJob` + Represents a DPS processing job. + +Submodules +---------- + +:mod:`maap.maap` + Main MAAP client module. + +:mod:`maap.Result` + CMR search result classes. + +:mod:`maap.dps` + Data Processing System job management. + +:mod:`maap.AWS` + AWS credential management. + +:mod:`maap.Profile` + User profile management. + +:mod:`maap.Secrets` + User secrets management. + +:mod:`maap.config_reader` + Configuration management. + +Environment Variables +--------------------- + +The following environment variables configure the client: + +- ``MAAP_API_HOST``: MAAP API hostname +- ``MAAP_PGT``: Proxy granting ticket for authentication +- ``MAAP_AWS_ACCESS_KEY_ID``: AWS access key +- ``MAAP_AWS_SECRET_ACCESS_KEY``: AWS secret key + +See Also +-------- +- MAAP Documentation: https://docs.maap-project.org +- GitHub Repository: https://github.com/MAAP-Project/maap-py +""" + +__version__ = "4.2.0" +__author__ = "NASA MAAP Project / Jet Propulsion Laboratory" +__license__ = "Apache-2.0" + +# Import main classes for convenient access +from maap.maap import MAAP + +__all__ = ["MAAP", "__version__"] diff --git a/maap/config_reader.py b/maap/config_reader.py index e8a2fbd..e2f82e3 100644 --- a/maap/config_reader.py +++ b/maap/config_reader.py @@ -1,3 +1,33 @@ +""" +Configuration Management +======================== + +This module provides the :class:`MaapConfig` class for managing MAAP client +configuration, including API endpoints, authentication tokens, and settings. + +The configuration is automatically fetched from the MAAP API server when +the client is initialized. + +Example +------- +Configuration is typically accessed through the MAAP client:: + + from maap.maap import MAAP + + maap = MAAP() + print(f"API Root: {maap.config.maap_api_root}") + print(f"Page Size: {maap.config.page_size}") + +Notes +----- +Configuration values are cached to avoid repeated API calls. Environment +variables can override certain settings. + +See Also +-------- +:class:`maap.maap.MAAP` : Main client class +""" + import logging import os import requests @@ -9,7 +39,15 @@ def _get_maap_api_host_url_scheme(): - # Check if OS ENV override exists, used by dev testing + """ + Get the URL scheme for MAAP API connections. + + Returns + ------- + str + URL scheme ('http' or 'https'). Defaults to 'https' unless + overridden by the ``MAAP_API_HOST_SCHEME`` environment variable. + """ scheme = os.environ.get("MAAP_API_HOST_SCHEME", None) if not scheme: logger.debug("No url scheme defined in env var MAAP_API_HOST_SCHEME; defaulting to 'https'.") @@ -18,15 +56,29 @@ def _get_maap_api_host_url_scheme(): def _get_config_url(maap_host): - # This is added to remove the assumption of scheme specially for local dev testing - # also maintains backwards compatibility for user to use MAAP("api.maap-project.org") + """ + Construct the configuration endpoint URL. + + Parameters + ---------- + maap_host : str + The MAAP API hostname, optionally with scheme. + + Returns + ------- + str + Full URL to the configuration endpoint. + + Raises + ------ + ValueError + If an unsupported URL scheme is provided. + """ base_url = urlparse(maap_host) maap_api_config_endpoint = os.getenv("MAAP_API_CONFIG_ENDPOINT", "api/environment/config") supported_schemes = ("http", "https") if base_url.scheme and base_url.scheme not in supported_schemes: raise ValueError(f"Unsupported scheme for MAAP API host: {base_url.scheme!r}. Must be one of: {', '.join(map(repr, supported_schemes))}.") - # If the netloc is empty, that means that the url does not contain scheme:// and the url parser would put the - # hostname in the path section. See https://docs.python.org/3.11/library/urllib.parse.html#urllib.parse.urlparse config_url = ( urljoin(maap_host, maap_api_config_endpoint) if base_url.netloc @@ -42,20 +94,47 @@ def _get_config_url(maap_host): def _get_api_root(config_url, config): - # Set maap api root to currently supplied maap host + """ + Determine the API root URL from configuration. + + Parameters + ---------- + config_url : str + The configuration endpoint URL. + config : dict + Configuration dictionary from the API. + + Returns + ------- + str + The API root URL with trailing slash. + """ api_root_url = urlparse(config.get("service").get("maap_api_root")) config_url = urlparse(config_url) - # Add trailing slash to api_root_url.path to ensure that urljoin does not remove it - # eg. urljoin("http://api.maap-project.org/api", "dps") will return http://api.dit.maap-project.org/dps - # But we want http://api.dit.maap-project.org/api/dps return SplitResult(scheme=config_url.scheme, netloc=config_url.netloc, path=api_root_url.path+"/", query='', fragment='').geturl() @cache def _get_client_config(maap_host): - # This is added to remove the assumption of scheme specially for local dev testing - # also maintains backwards compatibility for user to use MAAP("api.maap-project.org") + """ + Fetch and cache client configuration from the API. + + Parameters + ---------- + maap_host : str + The MAAP API hostname. + + Returns + ------- + dict + Configuration dictionary containing service endpoints and settings. + + Notes + ----- + Results are cached using functools.cache to avoid repeated API calls + for the same host. + """ config_url = _get_config_url(maap_host) logger.debug(f"Requesting client config from api at: {config_url}") response = requests.get(config_url) @@ -69,6 +148,107 @@ def _get_client_config(maap_host): class MaapConfig: + """ + MAAP client configuration manager. + + Manages all configuration settings for the MAAP client, including API + endpoints, authentication tokens, and operational parameters. + + Parameters + ---------- + maap_host : str + The MAAP API hostname (e.g., ``'api.maap-project.org'``). + + Attributes + ---------- + maap_host : str + The configured API hostname. + maap_api_root : str + Base URL for all API requests. + maap_token : str + Authentication token for API requests. + page_size : int + Number of results per page for CMR queries. + content_type : str + Default content type for CMR requests. + + Endpoint Attributes + ------------------- + algorithm_register : str + Endpoint for algorithm registration. + algorithm_build : str + Endpoint for algorithm builds. + mas_algo : str + Endpoint for algorithm management. + dps_job : str + Endpoint for DPS job operations. + member_dps_token : str + Endpoint for DPS token retrieval. + requester_pays : str + Endpoint for requester-pays credentials. + edc_credentials : str + Endpoint for Earthdata Cloud credentials. + workspace_bucket_credentials : str + Endpoint for workspace bucket credentials. + s3_signed_url : str + Endpoint for generating signed S3 URLs. + wmts : str + Endpoint for WMTS tile service. + member : str + Endpoint for member/profile operations. + search_granule_url : str + Endpoint for CMR granule searches. + search_collection_url : str + Endpoint for CMR collection searches. + + AWS Attributes + -------------- + aws_access_key : str or None + AWS access key from environment. + aws_access_secret : str or None + AWS secret key from environment. + s3_user_upload_bucket : str or None + S3 bucket for user uploads. + s3_user_upload_dir : str or None + S3 directory prefix for user uploads. + + Other Attributes + ---------------- + indexed_attributes : list + Custom indexed attributes for CMR searches. + mapbox_token : str + Mapbox access token for visualization. + tiler_endpoint : str + URL for the tile rendering service. + + Examples + -------- + Access configuration through MAAP client:: + + >>> maap = MAAP() + >>> print(f"API Root: {maap.config.maap_api_root}") + >>> print(f"Granule Search URL: {maap.config.search_granule_url}") + + Notes + ----- + Configuration is automatically loaded from the MAAP API when the client + is initialized. Many settings can be overridden via environment variables: + + - ``MAAP_API_HOST``: Override API hostname + - ``MAAP_CMR_PAGE_SIZE``: Override CMR page size + - ``MAAP_CMR_CONTENT_TYPE``: Override CMR content type + - ``MAAP_PGT``: Proxy granting ticket + - ``MAAP_AWS_ACCESS_KEY_ID``: AWS access key + - ``MAAP_AWS_SECRET_ACCESS_KEY``: AWS secret key + - ``MAAP_S3_USER_UPLOAD_BUCKET``: Upload bucket + - ``MAAP_S3_USER_UPLOAD_DIR``: Upload directory + - ``MAAP_MAPBOX_ACCESS_TOKEN``: Mapbox token + + See Also + -------- + :class:`~maap.maap.MAAP` : Main client class + """ + def __init__(self, maap_host): self.__config = _get_client_config(maap_host) self.maap_host = maap_host @@ -99,9 +279,36 @@ def __init__(self, maap_host): self.mapbox_token = os.environ.get("MAAP_MAPBOX_ACCESS_TOKEN", '') def _get_api_endpoint(self, config_key): - # Remove any prefix "/" for urljoin + """ + Construct a full API endpoint URL. + + Parameters + ---------- + config_key : str + The configuration key for the endpoint. + + Returns + ------- + str + Full URL for the endpoint. + """ endpoint = str(self.__config.get("maap_endpoint").get(config_key)).strip("/") return urljoin(self.maap_api_root, endpoint) def get(self, profile, key): + """ + Get a configuration value. + + Parameters + ---------- + profile : str + Configuration section name. + key : str + Configuration key within the section. + + Returns + ------- + any + The configuration value, or None if not found. + """ return self.__config.get(profile, key) diff --git a/maap/dps/dps_job.py b/maap/dps/dps_job.py index 8ad9e2f..6c395dd 100644 --- a/maap/dps/dps_job.py +++ b/maap/dps/dps_job.py @@ -1,3 +1,47 @@ +""" +DPS Job Management +================== + +This module provides the :class:`DPSJob` class for managing jobs submitted +to the MAAP Data Processing System (DPS). + +The DPSJob class allows users to: + +* Track job status (queued, running, completed, failed) +* Wait for job completion with automatic retry +* Retrieve job outputs and metrics +* Cancel running jobs + +Example +------- +Monitor a submitted job:: + + from maap.maap import MAAP + + maap = MAAP() + + # Submit a job + job = maap.submitJob( + identifier='my_analysis', + algo_id='my_algorithm', + version='main', + queue='maap-dps-worker-8gb' + ) + + # Wait for completion + job.wait_for_completion() + + # Check results + if job.status == 'Succeeded': + print(f"Outputs: {job.outputs}") + print(f"Duration: {job.job_duration_seconds} seconds") + +See Also +-------- +:meth:`maap.maap.MAAP.submitJob` : Submit a new job +:meth:`maap.maap.MAAP.getJob` : Retrieve an existing job +""" + import json import logging import os @@ -22,17 +66,103 @@ def _backoff_get_max_value(self): class DPSJob: """ - Sample Usage: - - job_id = 'f3780917-92c0-4440-8a84-9b28c2e64fa8' - job = DPSJob(True) - job.id = job_id - print(job.retrieve_status()) - print(job.retrieve_metrics()) - print(job.retrieve_result()) - job.dismiss_job() - job.delete_job() + Manage and monitor DPS job lifecycle. + + The DPSJob class represents a job submitted to the MAAP Data Processing + System. It provides methods to check status, retrieve results and metrics, + wait for completion, and cancel jobs. + + Parameters + ---------- + config : MaapConfig + Configuration object containing API endpoints and authentication. + not_self_signed : bool, optional + If ``True`` (default), verify SSL certificates. Set to ``False`` + for self-signed certificates (development only). + + Attributes + ---------- + id : str + Unique job identifier (UUID). + status : str + Current job status: 'Accepted', 'Running', 'Succeeded', 'Failed', + 'Dismissed', 'Deduped', or 'Offline'. + outputs : list of str + URLs to job output files (available after completion). + metrics : dict + Performance metrics (available after completion). + response_code : int + HTTP response code from job submission. + error_details : str + Error message if job failed. + + Metric Attributes + ----------------- + machine_type : str + EC2 instance type used for execution. + architecture : str + CPU architecture. + operating_system : str + Operating system used. + job_start_time : str + ISO timestamp when job started. + job_end_time : str + ISO timestamp when job completed. + job_duration_seconds : float + Total execution time in seconds. + cpu_usage : int + CPU time in nanoseconds. + mem_usage : int + Current memory usage in bytes. + max_mem_usage : int + Peak memory usage in bytes. + swap_usage : int + Swap memory usage in bytes. + cache_usage : int + Cache memory usage in bytes. + directory_size : int + Output directory size in bytes. + read_io_stats : int + Read I/O operations. + write_io_stats : int + Write I/O operations. + + Examples + -------- + Get job status:: + + >>> job = maap.getJob('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> print(f"Status: {job.status}") + + Wait for completion:: + + >>> job = maap.submitJob(...) + >>> job.wait_for_completion() + >>> print(f"Final status: {job.status}") + + Access metrics after completion:: + + >>> if job.status == 'Succeeded': + ... print(f"Duration: {job.job_duration_seconds}s") + ... print(f"Max memory: {job.max_mem_usage} bytes") + + Cancel a running job:: + + >>> job.cancel_job() + + Notes + ----- + - Jobs are executed asynchronously on the DPS infrastructure + - :meth:`wait_for_completion` uses exponential backoff (max 64s intervals) + - Metrics are only available for completed jobs + - The ``outputs`` list typically contains HTTP, S3, and console URLs + + See Also + -------- + :meth:`maap.maap.MAAP.submitJob` : Submit new jobs + :meth:`maap.maap.MAAP.listJobs` : List all jobs """ + def __init__(self, config: MaapConfig, not_self_signed=True): self.config = config self.__not_self_signed = not_self_signed @@ -61,11 +191,36 @@ def __init__(self, config: MaapConfig, not_self_signed=True): self.__outputs = [] self.__traceback = None self.__metrics = dict() - + def retrieve_status(self): - # Not using os.path.join just to be safe as this can break if ever run on windows - # not using urljoin as that requires more preprocessing to avoid dropping api root while joining - # eg. urljoing("https://api.maap-project.org/api/dps", "id/status") will drop "api/dps" from the output + """ + Retrieve the current status of the job. + + Queries the DPS API to get the latest job status. + + Returns + ------- + str + The current job status. Possible values: + + - ``'Accepted'``: Job is queued + - ``'Running'``: Job is executing + - ``'Succeeded'``: Job completed successfully + - ``'Failed'``: Job completed with errors + - ``'Dismissed'``: Job was cancelled + + Examples + -------- + :: + + >>> status = job.retrieve_status() + >>> print(f"Current status: {status}") + + See Also + -------- + :meth:`wait_for_completion` : Block until job completes + :meth:`retrieve_attributes` : Get status plus results/metrics + """ url = f"{self.config.dps_job}/{self.id}/{endpoints.DPS_JOB_STATUS}" response = requests_utils.make_dps_request(url, self.config) self.set_job_status_result(response) @@ -73,6 +228,38 @@ def retrieve_status(self): @backoff.on_exception(backoff.expo, Exception, max_value=64, max_time=172800) def wait_for_completion(self): + """ + Wait for the job to complete. + + Blocks execution until the job finishes (succeeds, fails, or is + cancelled). Uses exponential backoff to poll for status updates. + + Returns + ------- + DPSJob + Self, with updated status and attributes. + + Examples + -------- + :: + + >>> job = maap.submitJob(...) + >>> job.wait_for_completion() + >>> if job.status == 'Succeeded': + ... print("Job completed successfully!") + ... print(f"Outputs: {job.outputs}") + + Notes + ----- + - Uses exponential backoff with max interval of 64 seconds + - Maximum wait time is 48 hours (172800 seconds) + - The job object is updated with final status upon completion + + See Also + -------- + :meth:`retrieve_status` : Check status without blocking + :meth:`cancel_job` : Cancel a running job + """ self.retrieve_status() if self.status.lower() in ["accepted", "running"]: logger.debug('Current Status is {}. Backing off.'.format(self.status)) @@ -80,18 +267,113 @@ def wait_for_completion(self): return self def retrieve_result(self): + """ + Retrieve output URLs from a completed job. + + Gets the list of URLs pointing to job output files. + + Returns + ------- + list of str + URLs to output files. Typically includes: + + - HTTP URL for web browser access + - S3 URL for programmatic access + - AWS Console URL for S3 bucket browsing + + Examples + -------- + :: + + >>> outputs = job.retrieve_result() + >>> for url in outputs: + ... print(url) + + Notes + ----- + Results are only available for completed jobs. For running jobs, + this will return an empty list. + + See Also + -------- + :meth:`retrieve_metrics` : Get performance metrics + :meth:`retrieve_attributes` : Get all job information + """ url = f"{self.config.dps_job}/{self.id}" response = requests_utils.make_dps_request(url, self.config) self.set_job_results_result(response) return self.outputs def retrieve_metrics(self): + """ + Retrieve performance metrics from a completed job. + + Gets resource usage and timing information for the job. + + Returns + ------- + dict + Dictionary containing job metrics including execution time, + memory usage, CPU usage, and I/O statistics. + + Examples + -------- + :: + + >>> metrics = job.retrieve_metrics() + >>> print(f"Machine: {metrics['machine_type']}") + >>> print(f"Duration: {metrics['job_duration_seconds']}s") + >>> print(f"Max memory: {metrics['max_mem_usage']} bytes") + + Notes + ----- + Metrics are only available for completed jobs. Some metrics may + be unavailable if the job failed early in execution. + + See Also + -------- + :meth:`retrieve_result` : Get output URLs + :meth:`retrieve_attributes` : Get all job information + """ url = f"{self.config.dps_job}/{self.id}/{endpoints.DPS_JOB_METRICS}" response = requests_utils.make_dps_request(url, self.config) self.set_job_metrics_result(response) return self.metrics def retrieve_attributes(self): + """ + Retrieve all available job attributes. + + Fetches the job status, and if the job is complete, also retrieves + results and metrics. + + Returns + ------- + DPSJob + Self, with all available attributes populated. + + Examples + -------- + :: + + >>> job.retrieve_attributes() + >>> print(f"Status: {job.status}") + >>> if job.status == 'Succeeded': + ... print(f"Outputs: {job.outputs}") + ... print(f"Duration: {job.job_duration_seconds}s") + + Notes + ----- + This is a convenience method that calls :meth:`retrieve_status`, + :meth:`retrieve_result`, and :meth:`retrieve_metrics` as appropriate. + Errors during result/metrics retrieval are silently ignored. + + See Also + -------- + :meth:`retrieve_status` : Get status only + :meth:`retrieve_result` : Get outputs only + :meth:`retrieve_metrics` : Get metrics only + """ self.retrieve_status() if self.status.lower() in ["succeeded", "failed"]: try: @@ -106,6 +388,38 @@ def retrieve_attributes(self): return self def cancel_job(self): + """ + Cancel a running or queued job. + + Requests cancellation of this job. Jobs that are already complete + cannot be cancelled. + + Returns + ------- + str + Response from the DPS API indicating the result of the + cancellation request. + + Examples + -------- + :: + + >>> result = job.cancel_job() + >>> print(result) + >>> # Check updated status + >>> job.retrieve_status() + >>> print(f"Status after cancel: {job.status}") + + Notes + ----- + - Cancellation may not be immediate; the job will transition to + ``'Dismissed'`` status + - Resources allocated to the job will be released + + See Also + -------- + :meth:`retrieve_status` : Check job status after cancellation + """ url = f"{self.config.dps_job}/{endpoints.DPS_JOB_DISMISS}/{self.id}" response = requests_utils.make_dps_request(url, self.config, request_type=requests_utils.POST) return response diff --git a/maap/maap.py b/maap/maap.py index f8683eb..22a805b 100644 --- a/maap/maap.py +++ b/maap/maap.py @@ -1,3 +1,49 @@ +""" +MAAP Python Client +================== + +This module provides the main entry point for interacting with the NASA MAAP +(Multi-Mission Algorithm and Analysis Platform) API. + +The :class:`MAAP` class is the primary interface for all MAAP operations including: + +* Searching for granules and collections in CMR (Common Metadata Repository) +* Registering, managing, and executing algorithms on the Data Processing System (DPS) +* Managing user secrets and AWS credentials +* Uploading and downloading files +* Visualizing geospatial data + +Example +------- +Basic usage:: + + from maap.maap import MAAP + + # Initialize the MAAP client + maap = MAAP() + + # Search for granules + granules = maap.searchGranule( + short_name='GEDI02_A', + limit=10 + ) + + # Download a granule + for granule in granules: + local_path = granule.getData() + +Note +---- +The MAAP client automatically reads configuration from the MAAP API endpoint. +Authentication is handled via environment variables or the MAAP platform. + +See Also +-------- +:class:`maap.Result.Granule` : Class representing a CMR granule +:class:`maap.Result.Collection` : Class representing a CMR collection +:class:`maap.dps.dps_job.DPSJob` : Class representing a DPS job +""" + import json import logging import boto3 @@ -28,6 +74,85 @@ class MAAP(object): + """ + Main client class for interacting with the MAAP API. + + The MAAP class provides a unified interface for all MAAP platform operations, + including data discovery, algorithm management, job submission, and file operations. + + Parameters + ---------- + maap_host : str, optional + The hostname of the MAAP API server. Defaults to the value of the + ``MAAP_API_HOST`` environment variable, or ``'api.maap-project.org'`` + if not set. + + Attributes + ---------- + config : MaapConfig + Configuration object containing API endpoints and settings. + profile : Profile + Interface for user profile operations. + aws : AWS + Interface for AWS credential operations. + secrets : Secrets + Interface for user secrets management. + + Examples + -------- + Initialize with default settings:: + + >>> from maap.maap import MAAP + >>> maap = MAAP() + + Initialize with a custom host:: + + >>> maap = MAAP(maap_host='api.ops.maap-project.org') + + Search for granules:: + + >>> granules = maap.searchGranule( + ... short_name='GEDI02_A', + ... bounding_box='-122.5,37.5,-121.5,38.5', + ... limit=5 + ... ) + >>> for g in granules: + ... print(g.getDescription()) + + Submit a job:: + + >>> job = maap.submitJob( + ... identifier='my_analysis', + ... algo_id='my_algorithm', + ... version='main', + ... queue='maap-dps-worker-8gb', + ... input_file='s3://bucket/input.tif' + ... ) + >>> print(f"Job submitted: {job.id}") + + Notes + ----- + The MAAP client requires proper authentication to access most features. + Authentication is typically handled automatically when running within + the MAAP Algorithm Development Environment (ADE). + + Environment Variables + --------------------- + MAAP_API_HOST : str + Override the default MAAP API host. + MAAP_PGT : str + Proxy Granting Ticket for authentication. + MAAP_AWS_ACCESS_KEY_ID : str + AWS access key for S3 operations. + MAAP_AWS_SECRET_ACCESS_KEY : str + AWS secret key for S3 operations. + + See Also + -------- + :class:`maap.Profile.Profile` : User profile management + :class:`maap.AWS.AWS` : AWS credential management + :class:`maap.Secrets.Secrets` : User secrets management + """ def __init__(self, maap_host=os.getenv('MAAP_API_HOST', 'api.maap-project.org')): self.config = MaapConfig(maap_host=maap_host) @@ -45,7 +170,33 @@ def __init__(self, maap_host=os.getenv('MAAP_API_HOST', 'api.maap-project.org')) self.secrets = Secrets(self.config.member, self._get_api_header(content_type="application/json")) def _get_api_header(self, content_type=None): + """ + Generate HTTP headers for API requests. + + Constructs the authorization and content-type headers required for + making authenticated requests to the MAAP API. + + Parameters + ---------- + content_type : str, optional + The content type for the request. If not specified, uses the + default content type from configuration. + + Returns + ------- + dict + Dictionary containing HTTP headers including: + - ``Accept``: The expected response content type + - ``Content-Type``: The request content type + - ``token``: The MAAP authentication token + - ``proxy-ticket``: The proxy granting ticket (if available) + Notes + ----- + This is an internal method used by other MAAP methods to construct + proper API request headers. The proxy ticket is automatically included + if the ``MAAP_PGT`` environment variable is set. + """ api_header = {'Accept': content_type if content_type else self.config.content_type, 'token': self.config.maap_token, 'Content-Type': content_type if content_type else self.config.content_type} if os.environ.get("MAAP_PGT"): @@ -55,22 +206,120 @@ def _get_api_header(self, content_type=None): def _upload_s3(self, filename, bucket, objectKey): """ - Upload file to S3, utility function useful for mocking in tests. - :param filename (string) - local filename (and path) - :param bucket (string) - S3 bucket to upload to - :param objectKey (string) - S3 directory and filename to upload the local file to - :return: S3 upload_file response + Upload a file to Amazon S3. + + Internal utility method for uploading files to S3 storage. + + Parameters + ---------- + filename : str + Local path to the file to upload. + bucket : str + Name of the S3 bucket to upload to. + objectKey : str + The S3 object key (path) where the file will be stored. + + Returns + ------- + dict + S3 upload response containing upload metadata. + + Notes + ----- + This is an internal method primarily used by :meth:`uploadFiles`. + It uses the boto3 S3 client configured at module level. """ return s3_client.upload_file(filename, bucket, objectKey) def searchGranule(self, limit=20, **kwargs): """ - Search the CMR granules + Search for granules in the CMR (Common Metadata Repository). + + Queries the CMR database for granules matching the specified criteria. + Granules represent individual data files within a collection. + + Parameters + ---------- + limit : int, optional + Maximum number of results to return. Default is 20. + **kwargs : dict + Search parameters to filter results. Common parameters include: + + short_name : str + Collection short name (e.g., 'GEDI02_A'). + collection_concept_id : str + Unique CMR collection identifier. + bounding_box : str + Spatial filter as 'west,south,east,north' coordinates. + temporal : str + Temporal filter as 'start_date,end_date' in ISO format. + polygon : str + Polygon coordinates for spatial filtering. + readable_granule_name : str + Filter by granule name pattern. Supports wildcards. + instrument : str + Filter by instrument name (e.g., 'uavsar'). + platform : str + Filter by platform name (e.g., 'GEDI'). + site_name : str + Filter by site name for MAAP-indexed datasets. + + Returns + ------- + list of Granule + List of :class:`~maap.Result.Granule` objects matching the search + criteria. Each granule provides methods to access download URLs + and retrieve data. + + Examples + -------- + Search by collection name:: + + >>> granules = maap.searchGranule( + ... short_name='GEDI02_A', + ... limit=10 + ... ) + + Search with spatial bounds:: + + >>> granules = maap.searchGranule( + ... collection_concept_id='C1234567890-MAAP', + ... bounding_box='-122.5,37.5,-121.5,38.5', + ... limit=5 + ... ) + + Search with temporal filter:: + + >>> granules = maap.searchGranule( + ... short_name='AFLVIS2', + ... temporal='2019-01-01T00:00:00Z,2019-12-31T23:59:59Z', + ... limit=100 + ... ) + + Search with pattern matching:: + + >>> granules = maap.searchGranule( + ... readable_granule_name='*2019*', + ... short_name='GEDI02_A' + ... ) + + Download results:: + + >>> for granule in granules: + ... print(granule.getDescription()) + ... local_path = granule.getData(destpath='/tmp') + + Notes + ----- + - Multiple search parameters can be combined with pipe (``|``) delimiter. + - Wildcard characters (``*``, ``?``) are supported for pattern matching. + - Results are automatically paginated internally. - :param limit: limit of the number of results - :param kwargs: search parameters - :return: list of results () - """ + See Also + -------- + :meth:`searchCollection` : Search for collections + :class:`~maap.Result.Granule` : Granule result class + """ results = self._CMR.get_search_results(url=self.config.search_granule_url, limit=limit, **kwargs) return [Granule(result, self.config.aws_access_key, @@ -81,14 +330,62 @@ def searchGranule(self, limit=20, **kwargs): def downloadGranule(self, online_access_url, destination_path=".", overwrite=False): """ - Direct download of http Earthdata granule URL (protected or public). + Download a granule directly from an HTTP URL. + + Downloads data from an Earthdata HTTP URL, handling both public and + protected (authenticated) resources automatically. + + Parameters + ---------- + online_access_url : str + The HTTP URL of the granule to download. This is typically obtained + from a granule's ``OnlineAccessURL`` field. + destination_path : str, optional + Directory path where the file will be saved. Default is the current + working directory (``'.'``). + overwrite : bool, optional + If ``True``, overwrite existing files. If ``False`` (default), skip + download if the file already exists. + + Returns + ------- + str + The local file path of the downloaded file. + + Examples + -------- + Download a granule by URL:: + + >>> local_file = maap.downloadGranule( + ... 'https://data.maap-project.org/file/data.h5', + ... destination_path='/tmp/downloads' + ... ) + >>> print(f"Downloaded to: {local_file}") + + Force overwrite of existing files:: + + >>> local_file = maap.downloadGranule( + ... url, + ... destination_path='/tmp', + ... overwrite=True + ... ) - :param online_access_url: the value of the granule's http OnlineAccessURL - :param destination_path: use the current directory as default - :param overwrite: don't download by default if the target file exists - :return: the file path of the download file - """ + Notes + ----- + This method handles authentication automatically: + - First attempts an unauthenticated request + - Falls back to EDL (Earthdata Login) federated authentication if needed + - Uses DPS machine tokens when running inside a DPS job + + For most use cases, prefer using :meth:`Granule.getData()` instead, + which handles URL selection automatically. + + See Also + -------- + :meth:`searchGranule` : Search for granules + :meth:`~maap.Result.Granule.getData` : Download granule data + """ filename = os.path.basename(urllib.parse.urlparse(online_access_url).path) destination_file = filename.replace("/", "") final_destination = os.path.join(destination_path, destination_file) @@ -102,38 +399,213 @@ def downloadGranule(self, online_access_url, destination_path=".", overwrite=Fal def getCallFromEarthdataQuery(self, query, variable_name='maap', limit=1000): """ - Generate a literal string to use for calling the MAAP API + Generate a MAAP API call string from an Earthdata search query. + + Converts a JSON-formatted Earthdata search query into a Python code + string that can be used to call the MAAP API. + + Parameters + ---------- + query : str + A JSON-formatted string representing an Earthdata search query. + This is the format used by the Earthdata Search application. + variable_name : str, optional + The variable name to use in the generated code for the MAAP + client instance. Default is ``'maap'``. + limit : int, optional + Maximum number of records to return. Default is 1000. + + Returns + ------- + str + A Python code string that can be executed to perform the + equivalent MAAP API search. + + Examples + -------- + Convert an Earthdata query:: + + >>> query = '{"instrument_h": ["GEDI"], "bounding_box": "-180,-90,180,90"}' + >>> code = maap.getCallFromEarthdataQuery(query) + >>> print(code) + maap.searchGranule(instrument="GEDI", bounding_box="-180,-90,180,90", limit=1000) - :param query: a Json-formatted string from an Earthdata search-style query. See: https://github.com/MAAP-Project/earthdata-search/blob/master/app/controllers/collections_controller.rb - :param variable_name: the name of the MAAP variable to qualify the search call - :param limit: the max records to return - :return: string in the form of a MAAP API call - """ + Notes + ----- + This is useful for converting queries from the Earthdata Search + web interface to MAAP API calls. The generated string can be + executed using ``eval()`` or used as a reference. + + See Also + -------- + :meth:`getCallFromCmrUri` : Generate call from CMR URI + :meth:`searchGranule` : Execute a granule search + """ return self._CMR.generateGranuleCallFromEarthDataRequest(query, variable_name, limit) def getCallFromCmrUri(self, search_url, variable_name='maap', limit=1000, search='granule'): """ - Generate a literal string to use for calling the MAAP API + Generate a MAAP API call string from a CMR REST API URL. + + Converts a CMR REST API query URL into a Python code string that can + be used to call the MAAP API. + + Parameters + ---------- + search_url : str + A CMR REST API search URL. This can be copied directly from + the CMR API or browser address bar. + variable_name : str, optional + The variable name to use in the generated code for the MAAP + client instance. Default is ``'maap'``. + limit : int, optional + Maximum number of records to return. Default is 1000. + search : str, optional + Type of search to perform. Either ``'granule'`` (default) or + ``'collection'``. + + Returns + ------- + str + A Python code string that can be executed to perform the + equivalent MAAP API search. + + Examples + -------- + Convert a CMR granule search URL:: + + >>> url = 'https://cmr.earthdata.nasa.gov/search/granules?short_name=GEDI02_A' + >>> code = maap.getCallFromCmrUri(url) + >>> print(code) + maap.searchGranule(short_name="GEDI02_A", limit=1000) + + Convert a collection search:: - :param search_url: a Json-formatted string from an Earthdata search-style query. See: https://github.com/MAAP-Project/earthdata-search/blob/master/app/controllers/collections_controller.rb - :param variable_name: the name of the MAAP variable to qualify the search call - :param limit: the max records to return - :param search: defaults to 'granule' search, otherwise can be a 'collection' search - :return: string in the form of a MAAP API call - """ + >>> url = 'https://cmr.earthdata.nasa.gov/search/collections?provider=MAAP' + >>> code = maap.getCallFromCmrUri(url, search='collection') + >>> print(code) + maap.searchCollection(provider="MAAP", limit=1000) + + Notes + ----- + This is useful for converting existing CMR queries to MAAP API calls. + Duplicate query parameters are automatically converted to pipe-delimited + values. + + See Also + -------- + :meth:`getCallFromEarthdataQuery` : Generate call from Earthdata query + :meth:`searchGranule` : Execute a granule search + :meth:`searchCollection` : Execute a collection search + """ return self._CMR.generateCallFromEarthDataQueryString(search_url, variable_name, limit, search) def searchCollection(self, limit=100, **kwargs): """ - Search the CMR collections - :param limit: limit of the number of results - :param kwargs: search parameters - :return: list of results () + Search for collections in the CMR (Common Metadata Repository). + + Queries the CMR database for collections (datasets) matching the + specified criteria. Collections represent groups of related data files. + + Parameters + ---------- + limit : int, optional + Maximum number of results to return. Default is 100. + **kwargs : dict + Search parameters to filter results. Common parameters include: + + short_name : str + Collection short name (e.g., 'GEDI02_A'). + concept_id : str + Unique CMR collection identifier. + provider : str + Data provider (e.g., 'MAAP', 'LPDAAC_ECS'). + keyword : str + Keyword search across collection metadata. + instrument : str + Filter by instrument name. + platform : str + Filter by platform name. + project : str + Filter by project name. + processing_level_id : str + Filter by data processing level. + + Returns + ------- + list of Collection + List of :class:`~maap.Result.Collection` objects matching the + search criteria. + + Examples + -------- + Search by short name:: + + >>> collections = maap.searchCollection(short_name='GEDI02_A') + >>> for c in collections: + ... print(c['Collection']['ShortName']) + + Search by provider:: + + >>> collections = maap.searchCollection( + ... provider='MAAP', + ... limit=50 + ... ) + + Search by keyword:: + + >>> collections = maap.searchCollection( + ... keyword='biomass forest', + ... limit=20 + ... ) + + Notes + ----- + Collections contain metadata about datasets but not the actual data + files. Use :meth:`searchGranule` to find individual data files within + a collection. + + See Also + -------- + :meth:`searchGranule` : Search for granules within collections + :class:`~maap.Result.Collection` : Collection result class """ results = self._CMR.get_search_results(url=self.config.search_collection_url, limit=limit, **kwargs) return [Collection(result, self.config.maap_host) for result in results][:limit] def getQueues(self): + """ + Get available DPS processing queues (resources). + + Retrieves a list of available compute resources (queues) that can be + used for algorithm execution. Different queues provide different + amounts of memory and CPU. + + Returns + ------- + requests.Response + HTTP response containing JSON list of available queues. Each queue + entry includes resource specifications like memory and CPU limits. + + Examples + -------- + List available queues:: + + >>> response = maap.getQueues() + >>> queues = response.json() + >>> for queue in queues: + ... print(f"{queue['name']}: {queue['memory']} RAM") + + Notes + ----- + Common queue names follow the pattern ``maap-dps-worker-{size}`` + where size indicates memory (e.g., ``8gb``, ``16gb``, ``32gb``). + + See Also + -------- + :meth:`submitJob` : Submit a job to a queue + :meth:`registerAlgorithm` : Register an algorithm to run on queues + """ url = os.path.join(self.config.algorithm_register, 'resource') headers = self._get_api_header() logger.debug('GET request sent to {}'.format(self.config.algorithm_register)) @@ -146,6 +618,75 @@ def getQueues(self): return response def registerAlgorithm(self, arg): + """ + Register an algorithm with the MAAP DPS. + + Registers a new algorithm configuration that can be executed on the + MAAP Data Processing System (DPS). + + Parameters + ---------- + arg : dict or str + Algorithm configuration as a dictionary or JSON string. Required + fields include: + + algorithm_name : str + Unique name for the algorithm. + code_version : str + Version identifier (e.g., Git branch or tag). + algorithm_description : str + Human-readable description. + docker_container_url : str + URL of the Docker container image. + script_command : str + Command to execute inside the container. + inputs : list of dict + Input parameter definitions with ``field`` and ``download`` keys. Format should be like + {'file': [{'name': 'input_file'}],'config': [{'name': 'config_param'}],'positional': [{'name': 'pos_arg'}]} + repo_url : str + Git repository URL for the algorithm source code. + + Returns + ------- + requests.Response + HTTP response indicating success or failure of registration. + + Examples + -------- + Register using a dictionary:: + + >>> config = { + ... 'algorithm_name': 'my_algorithm', + ... 'code_version': 'main', + ... 'algorithm_description': 'Processes satellite data', + ... 'docker_container_url': 'registry/image:tag', + ... 'script_command': 'python run.py', + ... 'inputs': { + ... 'file': [{'name': 'input_file'}], + ... 'config': [{'name': 'config_param'}], + ... 'positional': [{'name': 'pos_arg'}] + ... }, + ... 'repo_url': 'https://github.com/org/repo' + ... } + >>> response = maap.registerAlgorithm(config) + + Register using a JSON string:: + + >>> import json + >>> response = maap.registerAlgorithm(json.dumps(config)) + + Notes + ----- + After registration, algorithms need to be built before they can be + executed. The build process creates the Docker image on the DPS + infrastructure. + + See Also + -------- + :meth:`register_algorithm_from_yaml_file` : Register from YAML file + :meth:`listAlgorithms` : List registered algorithms + :meth:`deleteAlgorithm` : Delete an algorithm + """ logger.debug('Registering algorithm with args ') if type(arg) is dict: arg = json.dumps(arg) @@ -157,10 +698,87 @@ def registerAlgorithm(self, arg): return response def register_algorithm_from_yaml_file(self, file_path): + """ + Register an algorithm from a YAML configuration file. + + Reads algorithm configuration from a YAML file and registers it with + the MAAP DPS. + + Parameters + ---------- + file_path : str + Path to the YAML configuration file. + + Returns + ------- + requests.Response + HTTP response indicating success or failure of registration. + + Examples + -------- + Register from a YAML file:: + + >>> response = maap.register_algorithm_from_yaml_file('algorithm.yaml') + + Example YAML file structure:: + + algorithm_name: my_algorithm + code_version: main + algorithm_description: Process satellite data + docker_container_url: registry/image:tag + script_command: python run.py + inputs: + file: + - name: input_file + config: + - name: config_param + positional: + - name: pos_arg + repo_url: https://github.com/org/repo + + See Also + -------- + :meth:`registerAlgorithm` : Register from dict or JSON + :meth:`register_algorithm_from_yaml_file_backwards_compatible` : Legacy format + """ algo_config = algorithm_utils.read_yaml_file(file_path) return self.registerAlgorithm(algo_config) def register_algorithm_from_yaml_file_backwards_compatible(self, file_path): + """ + Register an algorithm from a legacy YAML configuration file. + + Reads algorithm configuration from an older YAML format and converts + it to the current format before registration. + + Parameters + ---------- + file_path : str + Path to the legacy YAML configuration file. + + Returns + ------- + requests.Response + HTTP response indicating success or failure of registration. + + Notes + ----- + This method supports the legacy YAML format with different field names: + + - ``algo_name`` -> ``algorithm_name`` + - ``version`` -> ``code_version`` + - ``environment`` -> ``environment_name`` + - ``description`` -> ``algorithm_description`` + - ``docker_url`` -> ``docker_container_url`` + - ``inputs`` -> ``algorithm_params`` + - ``run_command`` -> ``script_command`` + - ``repository_url`` -> ``repo_url`` + + See Also + -------- + :meth:`register_algorithm_from_yaml_file` : Current format + :meth:`registerAlgorithm` : Register from dict + """ algo_yaml = algorithm_utils.read_yaml_file(file_path) key_map = {"algo_name": "algorithm_name", "version": "code_version", "environment": "environment_name", "description": "algorithm_description", "docker_url": "docker_container_url", @@ -181,6 +799,33 @@ def register_algorithm_from_yaml_file_backwards_compatible(self, file_path): return self.registerAlgorithm(json.dumps(output_config)) def listAlgorithms(self): + """ + List all registered algorithms. + + Retrieves a list of all algorithms registered by the current user + on the MAAP DPS. + + Returns + ------- + requests.Response + HTTP response containing JSON list of algorithms. Each algorithm + entry includes name, version, description, and status information. + + Examples + -------- + List all algorithms:: + + >>> response = maap.listAlgorithms() + >>> algorithms = response.json() + >>> for algo in algorithms: + ... print(f"{algo['algorithm_name']}:{algo['code_version']}") + + See Also + -------- + :meth:`describeAlgorithm` : Get details for specific algorithm + :meth:`registerAlgorithm` : Register a new algorithm + :meth:`deleteAlgorithm` : Delete an algorithm + """ url = self.config.mas_algo headers = self._get_api_header() logger.debug('GET request sent to {}'.format(url)) @@ -193,6 +838,37 @@ def listAlgorithms(self): return response def describeAlgorithm(self, algoid): + """ + Get detailed information about a registered algorithm. + + Retrieves the full configuration and status of a specific algorithm. + + Parameters + ---------- + algoid : str + The algorithm identifier, typically in the format + ``algorithm_name:code_version``. + + Returns + ------- + requests.Response + HTTP response containing JSON with algorithm details including + configuration, build status, and parameter definitions. + + Examples + -------- + Get algorithm details:: + + >>> response = maap.describeAlgorithm('my_algorithm:main') + >>> details = response.json() + >>> print(f"Description: {details['algorithm_description']}") + >>> print(f"Docker: {details['docker_container_url']}") + + See Also + -------- + :meth:`listAlgorithms` : List all algorithms + :meth:`publishAlgorithm` : Publish an algorithm + """ url = os.path.join(self.config.mas_algo, algoid) headers = self._get_api_header() logger.debug('GET request sent to {}'.format(url)) @@ -205,6 +881,41 @@ def describeAlgorithm(self, algoid): return response def publishAlgorithm(self, algoid): + """ + Publish an algorithm for public use. + + Makes a registered algorithm available for other MAAP users to + discover and execute. + + Parameters + ---------- + algoid : str + The algorithm identifier to publish, typically in the format + ``algorithm_name:code_version``. + + Returns + ------- + requests.Response + HTTP response indicating success or failure of publication. + + Examples + -------- + Publish an algorithm:: + + >>> response = maap.publishAlgorithm('my_algorithm:v1.0') + >>> if response.ok: + ... print("Algorithm published successfully") + + Notes + ----- + Published algorithms are visible to all MAAP users and can be + executed by anyone with DPS access. + + See Also + -------- + :meth:`registerAlgorithm` : Register an algorithm + :meth:`deleteAlgorithm` : Delete an algorithm + """ url = self.config.mas_algo.replace('algorithm', 'publish') headers = self._get_api_header() body = { "algo_id": algoid} @@ -221,6 +932,41 @@ def publishAlgorithm(self, algoid): return response def deleteAlgorithm(self, algoid): + """ + Delete a registered algorithm. + + Removes an algorithm registration from the MAAP DPS. This does not + affect any completed jobs that used the algorithm. + + Parameters + ---------- + algoid : str + The algorithm identifier to delete, typically in the format + ``algorithm_name:code_version``. + + Returns + ------- + requests.Response + HTTP response indicating success or failure of deletion. + + Examples + -------- + Delete an algorithm:: + + >>> response = maap.deleteAlgorithm('my_algorithm:main') + >>> if response.ok: + ... print("Algorithm deleted") + + Warnings + -------- + This action cannot be undone. The algorithm configuration will be + permanently removed. + + See Also + -------- + :meth:`registerAlgorithm` : Register an algorithm + :meth:`listAlgorithms` : List algorithms + """ url = os.path.join(self.config.mas_algo, algoid) headers = self._get_api_header() logger.debug('DELETE request sent to {}'.format(url)) @@ -234,62 +980,292 @@ def deleteAlgorithm(self, algoid): def getJob(self, jobid): + """ + Get a DPS job with all available attributes. + + Retrieves a job object with its current status, results (if available), + and metrics (if available). + + Parameters + ---------- + jobid : str + The unique job identifier (UUID). + + Returns + ------- + DPSJob + A :class:`~maap.dps.dps_job.DPSJob` object with populated attributes + including status, outputs, and metrics. + + Examples + -------- + Get a job and inspect its status:: + + >>> job = maap.getJob('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> print(f"Status: {job.status}") + >>> print(f"Outputs: {job.outputs}") + >>> print(f"Duration: {job.job_duration_seconds} seconds") + + See Also + -------- + :meth:`getJobStatus` : Get status only + :meth:`getJobResult` : Get results only + :meth:`getJobMetrics` : Get metrics only + :meth:`submitJob` : Submit a new job + """ job = DPSJob(self.config) job.id = jobid job.retrieve_attributes() return job def getJobStatus(self, jobid): + """ + Get the current status of a DPS job. + + Parameters + ---------- + jobid : str + The unique job identifier (UUID). + + Returns + ------- + str + The job status. Possible values are: + + - ``'Accepted'``: Job is queued + - ``'Running'``: Job is executing + - ``'Succeeded'``: Job completed successfully + - ``'Failed'``: Job failed + - ``'Dismissed'``: Job was cancelled + + Examples + -------- + Check job status:: + + >>> status = maap.getJobStatus('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> print(f"Job status: {status}") + + See Also + -------- + :meth:`getJob` : Get full job object + :meth:`cancelJob` : Cancel a running job + """ job = DPSJob(self.config) job.id = jobid return job.retrieve_status() def getJobResult(self, jobid): + """ + Get the output URLs from a completed DPS job. + + Parameters + ---------- + jobid : str + The unique job identifier (UUID). + + Returns + ------- + list of str + List of URLs pointing to job output files. Typically includes + HTTP, S3, and console URLs for the output directory. + + Examples + -------- + Get job outputs:: + + >>> outputs = maap.getJobResult('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> for url in outputs: + ... print(url) + + Notes + ----- + This method only returns results for jobs that have completed + (succeeded or failed). For running jobs, the output list will be empty. + + See Also + -------- + :meth:`getJob` : Get full job object + :meth:`getJobMetrics` : Get job performance metrics + """ job = DPSJob(self.config) job.id = jobid return job.retrieve_result() def getJobMetrics(self, jobid): + """ + Get performance metrics from a completed DPS job. + + Retrieves resource usage and timing information for a job. + + Parameters + ---------- + jobid : str + The unique job identifier (UUID). + + Returns + ------- + dict + Dictionary containing job metrics including: + + - ``machine_type``: EC2 instance type used + - ``job_start_time``: ISO timestamp of job start + - ``job_end_time``: ISO timestamp of job end + - ``job_duration_seconds``: Total execution time + - ``cpu_usage``: CPU time in nanoseconds + - ``mem_usage``: Memory usage in bytes + - ``max_mem_usage``: Peak memory usage in bytes + - ``directory_size``: Output directory size in bytes + + Examples + -------- + Get job metrics:: + + >>> metrics = maap.getJobMetrics('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> print(f"Duration: {metrics['job_duration_seconds']} seconds") + >>> print(f"Max memory: {metrics['max_mem_usage']} bytes") + + See Also + -------- + :meth:`getJob` : Get full job object + :meth:`getJobResult` : Get job outputs + """ job = DPSJob(self.config) job.id = jobid return job.retrieve_metrics() def cancelJob(self, jobid): + """ + Cancel a running or queued DPS job. + + Attempts to stop execution of a job that is currently running or + waiting in the queue. + + Parameters + ---------- + jobid : str + The unique job identifier (UUID) to cancel. + + Returns + ------- + str + Response from the DPS indicating the cancellation result. + + Examples + -------- + Cancel a job:: + + >>> result = maap.cancelJob('f3780917-92c0-4440-8a84-9b28c2e64fa8') + >>> print(result) + + Notes + ----- + Jobs that are already completed (Succeeded or Failed) cannot be + cancelled. The job status will be set to ``'Dismissed'`` upon + successful cancellation. + + See Also + -------- + :meth:`submitJob` : Submit a job + :meth:`getJobStatus` : Check job status + """ job = DPSJob(self.config) job.id = jobid return job.cancel_job() def listJobs(self, *, - algo_id=None, - end_time=None, - get_job_details=True, - offset=0, - page_size=10, + algo_id=None, + end_time=None, + get_job_details=True, + offset=0, + page_size=10, queue=None, start_time=None, status=None, - tag=None, + tag=None, version=None): """ - Returns a list of jobs for a given user that matches query params provided. + List jobs submitted by the current user. + + Retrieves a paginated list of DPS jobs matching the specified filter + criteria. + + Parameters + ---------- + algo_id : str, optional + Filter by algorithm name. Must be provided together with ``version``. + end_time : str, optional + Filter for jobs completed before this time. Format: ISO 8601 + (e.g., ``'2024-01-01'`` or ``'2024-01-01T00:00:00.000000Z'``). + get_job_details : bool, optional + If ``True`` (default), return detailed job information. If ``False``, + return only job IDs and tags for faster response. + offset : int, optional + Number of jobs to skip for pagination. Default is 0. + page_size : int, optional + Number of jobs to return per page. Default is 10. + queue : str, optional + Filter by processing queue name. + start_time : str, optional + Filter for jobs started after this time. Format: ISO 8601. + status : str, optional + Filter by job status. Valid values: + + - ``'Accepted'``: Queued jobs + - ``'Running'``: Currently executing + - ``'Succeeded'``: Completed successfully + - ``'Failed'``: Completed with errors + - ``'Dismissed'``: Cancelled jobs + + tag : str, optional + Filter by user-defined job tag/identifier. + version : str, optional + Filter by algorithm version. Must be provided together with ``algo_id``. + + Returns + ------- + requests.Response + HTTP response containing JSON list of jobs matching the criteria. + + Raises + ------ + ValueError + If only one of ``algo_id`` or ``version`` is provided. Both must + be provided together or neither should be provided. + + Examples + -------- + List recent jobs:: + + >>> response = maap.listJobs(page_size=20) + >>> jobs = response.json() + >>> for job in jobs: + ... print(f"{job['job_id']}: {job['status']}") + + Filter by algorithm and version:: - Args: - algo_id (str, optional): Algorithm type. - end_time (str, optional): Specifying this parameter will return all jobs that have completed from the provided end time to now. e.g. 2024-01-01 or 2024-01-01T00:00:00.000000Z. - get_job_details (bool, optional): Flag that determines whether to return a detailed job list or a compact list containing just the job ids and their associated job tags. Default is True. - offset (int, optional): Offset for pagination. Default is 0. - page_size (int, optional): Page size for pagination. Default is 10. - queue (str, optional): Job processing resource. - start_time (str, optional): Specifying this parameter will return all jobs that have started from the provided start time to now. e.g. 2024-01-01 or 2024-01-01T00:00:00.000000Z. - status (str, optional): Job status, e.g. job-completed, job-failed, job-started, job-queued. - tag (str, optional): User job tag/identifier. - version (str, optional): Algorithm version, e.g. GitHub branch or tag. + >>> response = maap.listJobs( + ... algo_id='my_algorithm', + ... version='main', + ... status='Succeeded' + ... ) - Returns: - list: List of jobs for a given user that matches query params provided. + Paginate through results:: - Raises: - ValueError: If either algo_id or version is provided, but not both. + >>> response = maap.listJobs(offset=0, page_size=10) + >>> # Get next page + >>> response = maap.listJobs(offset=10, page_size=10) + + Filter by time range:: + + >>> response = maap.listJobs( + ... start_time='2024-01-01', + ... end_time='2024-01-31' + ... ) + + See Also + -------- + :meth:`getJob` : Get details of a specific job + :meth:`submitJob` : Submit a new job """ url = "/".join( segment.strip("/") @@ -340,8 +1316,100 @@ def listJobs(self, *, return response def submitJob(self, identifier, algo_id, version, queue, retrieve_attributes=False, **kwargs): + """ + Submit a job to the MAAP Data Processing System (DPS). + + Submits an algorithm for execution on the DPS infrastructure with the + specified parameters and compute resources. + + Parameters + ---------- + identifier : str + A user-defined tag or identifier for the job. Used for tracking + and organizing jobs. + algo_id : str + The algorithm name to execute. + version : str + The algorithm version (e.g., Git branch or tag). + queue : str + The compute queue/resource to use (e.g., ``'maap-dps-worker-8gb'``). + Use :meth:`getQueues` to list available queues. + retrieve_attributes : bool, optional + If ``True``, immediately retrieve job status after submission. + Default is ``False``. + **kwargs : dict + Algorithm input parameters. Parameter names must match those + defined in the algorithm registration. + + Returns + ------- + DPSJob + A :class:`~maap.dps.dps_job.DPSJob` object representing the + submitted job. Use the job's methods to monitor status and + retrieve results. + + Examples + -------- + Submit a basic job:: + + >>> job = maap.submitJob( + ... identifier='my_analysis_run', + ... algo_id='my_algorithm', + ... version='main', + ... queue='maap-dps-worker-8gb', + ... input_file='s3://bucket/input.tif' + ... ) + >>> print(f"Job ID: {job.id}") + + Submit with multiple parameters:: + + >>> job = maap.submitJob( + ... identifier='batch_processing', + ... algo_id='processor', + ... version='v2.0', + ... queue='maap-dps-worker-32gb', + ... input_granule='s3://bucket/data.h5', + ... output_format='geotiff', + ... resolution=30 + ... ) + + Submit and immediately get status:: + + >>> job = maap.submitJob( + ... identifier='urgent_job', + ... algo_id='my_algorithm', + ... version='main', + ... queue='maap-dps-worker-8gb', + ... retrieve_attributes=True + ... ) + >>> print(f"Status: {job.status}") + + Monitor job completion:: + + >>> job = maap.submitJob(...) + >>> job.wait_for_completion() + >>> print(f"Final status: {job.status}") + >>> print(f"Outputs: {job.outputs}") + + Notes + ----- + - The job executes asynchronously; this method returns immediately + after submission. + - Use :meth:`~maap.dps.dps_job.DPSJob.wait_for_completion` to block + until the job finishes. + - Input parameters with ``download=True`` in the algorithm config + will be downloaded to the job's working directory. + + See Also + -------- + :meth:`getJob` : Retrieve job information + :meth:`listJobs` : List submitted jobs + :meth:`cancelJob` : Cancel a running job + :meth:`getQueues` : List available queues + :class:`~maap.dps.dps_job.DPSJob` : Job management class + """ # Note that this is temporary and will be removed when we remove the API not requiring username to submit a job - # Also this now overrides passing someone else's username into submitJob since we don't want to allow that + # Also this now overrides passing someone else's username into submitJob since we don't want to allow that if self.profile is not None and self.profile.account_info() is not None and 'username' in self.profile.account_info().keys(): kwargs['username'] = self.profile.account_info()['username'] response = self._DPS.submit_job(request_url=self.config.dps_job, @@ -357,21 +1425,68 @@ def submitJob(self, identifier, algo_id, version, queue, retrieve_attributes=Fal def uploadFiles(self, filenames): """ - Uploads files to a user-added staging directory. - Enables users of maap-py to potentially share files generated on the MAAP. - :param filenames: List of one or more filenames to upload - :return: String message including UUID of subdirectory of files + Upload files to MAAP shared storage. + + Uploads local files to an S3 staging directory where they can be + accessed by other MAAP users or used as inputs to DPS jobs. + + Parameters + ---------- + filenames : list of str + List of local file paths to upload. + + Returns + ------- + str + A message containing the UUID of the upload directory. This UUID + is needed to share the files with other users. + + Examples + -------- + Upload files to share:: + + >>> result = maap.uploadFiles(['data.csv', 'config.json']) + >>> print(result) + Upload file subdirectory: a1b2c3d4-e5f6-... (keep a record of...) + + Upload a single file:: + + >>> result = maap.uploadFiles(['output.tif']) + + Notes + ----- + - Files are uploaded to a unique subdirectory identified by a UUID + - Save the UUID to share the upload location with collaborators + - The upload location can be used as input to DPS jobs + + See Also + -------- + :meth:`submitJob` : Use uploaded files as job inputs """ bucket = self.config.s3_user_upload_bucket prefix = self.config.s3_user_upload_dir uuid_dir = uuid.uuid4() - # TODO(aimee): This should upload to a user-namespaced directory for filename in filenames: basename = os.path.basename(filename) response = self._upload_s3(filename, bucket, f"{prefix}/{uuid_dir}/{basename}") return f"Upload file subdirectory: {uuid_dir} (keep a record of this if you want to share these files with other users)" def _get_browse(self, granule_ur): + """ + Get browse image metadata for a granule. + + Internal method to retrieve browse image information for visualization. + + Parameters + ---------- + granule_ur : str + The Granule Universal Reference identifier. + + Returns + ------- + requests.Response + HTTP response containing browse image metadata. + """ response = requests.get( url=f'{self.config.wmts}/GetTile', params=dict(granule_ur=granule_ur), @@ -380,6 +1495,22 @@ def _get_browse(self, granule_ur): return response def _get_capabilities(self, granule_ur): + """ + Get WMTS capabilities for a granule. + + Internal method to retrieve Web Map Tile Service capabilities + for visualization. + + Parameters + ---------- + granule_ur : str + The Granule Universal Reference identifier. + + Returns + ------- + requests.Response + HTTP response containing WMTS capabilities XML. + """ response = requests.get( url=f'{self.config.wmts}/GetCapabilities', params=dict(granule_ur=granule_ur), @@ -388,6 +1519,49 @@ def _get_capabilities(self, granule_ur): return response def show(self, granule, display_config={}): + """ + Display a granule on an interactive map. + + Renders the granule data as a tile layer on an interactive Mapbox + map in a Jupyter notebook environment. + + Parameters + ---------- + granule : dict + A granule result dictionary, typically obtained from + :meth:`searchGranule`. Must contain ``Granule.GranuleUR``. + display_config : dict, optional + Configuration options for rendering. Common options include: + + rescale : str + Value range for color scaling (e.g., ``'0,70'``). + color_map : str + Color palette name (e.g., ``'schwarzwald'``). + + Examples + -------- + Display a granule on a map:: + + >>> granules = maap.searchGranule(short_name='AFLVIS2', limit=1) + >>> maap.show(granules[0]) + + Display with custom rendering:: + + >>> maap.show(granule, display_config={ + ... 'rescale': '0,100', + ... 'color_map': 'viridis' + ... }) + + Notes + ----- + - Requires ``mapboxgl`` package and a Jupyter notebook environment + - Uses the MAAP tile server for rendering + - A Mapbox access token must be configured + + See Also + -------- + :meth:`searchGranule` : Search for granules to visualize + """ from mapboxgl.viz import RasterTilesViz granule_ur = granule['Granule']['GranuleUR'] diff --git a/pyproject.toml b/pyproject.toml index 7fb5548..e15a40a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-py" -version = "4.2.0" +version = "4.3.0a1" description = "Python client API for interacting with the NASA MAAP API" repository = "https://github.com/MAAP-Project/maap-py" authors = ["Jet Propulsion Laboratory "]