diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 4bc27655e..ad76ba127 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -39,9 +39,7 @@ jobs: EARTHDATA_PASSWORD: "${{ secrets.EARTHDATA_PASSWORD }}" NSIDC_LOGIN: "${{ secrets.EARTHDATA_PASSWORD }}" run: | - pytest icepyx/ --verbose --cov app \ - icepyx/tests/test_behind_NSIDC_API_login.py \ - icepyx/tests/test_auth.py + pytest icepyx/tests/integration --verbose --cov app - name: "Upload coverage report" uses: "codecov/codecov-action@v4.5.0" diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 000000000..cfe884258 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,29 @@ +name: Typecheck + +on: + pull_request: + push: + branches: + - main + - development + + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install package and test dependencies + run: | + python -m pip install .[complete] + python -m pip install -r requirements-dev.txt + + - uses: jakebailey/pyright-action@v2 diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 989c7e298..8e2e186a2 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -8,6 +8,10 @@ on: branches: - "main" - "development" + workflow_run: + workflows: [Update UML diagrams] + types: + - completed jobs: @@ -31,8 +35,7 @@ jobs: - name: "Run tests" run: | pytest icepyx/ --verbose --cov app \ - --ignore=icepyx/tests/test_behind_NSIDC_API_login.py \ - --ignore=icepyx/tests/test_auth.py + --ignore=icepyx/tests/integration - name: "Upload coverage report" uses: "codecov/codecov-action@v4.5.0" diff --git a/doc/source/conf.py b/doc/source/conf.py index 0303f72ca..d51c371ee 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -32,13 +32,17 @@ # ones. extensions = [ "sphinx.ext.autodoc", + # IMPORTANT: napoleon must be loaded before sphinx_autodoc_typehints + # https://github.com/tox-dev/sphinx-autodoc-typehints/issues/15 + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", "sphinx.ext.autosectionlabel", "numpydoc", # "sphinx.ext.autosummary", "myst_nb", "contributors", # custom extension, from pandas "sphinxcontrib.bibtex", - "sphinx_panels", + "sphinx_design", # "sphinx.ext.imgconverter", # this extension should help the latex svg warning, but results in an error instead ] myst_enable_extensions = [ @@ -79,6 +83,31 @@ nb_execution_mode = "off" suppress_warnings = ["myst.header"] # suppress non-consecutive header warning + +# -- Options for Napoleon docstring parsing ---------------------------------- +napoleon_google_docstring = False +napoleon_numpy_docstring = True +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_notes = True + + +# -- Options for autodoc ----------------------------------------------------- + +# Show the typehints in the description of each object instead of the signature. +autodoc_typehints = "description" + + +# -- Options for autodoc typehints-------------------------------------------- + +# Replace Union annotations with union operator "|" +always_use_bars_union = True +# always_document_param_types = True + +# Show the default value for a parameter after its type +typehints_defaults = "comma" +typehints_use_return = True + + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/doc/source/index.rst b/doc/source/index.rst index e73818942..1630006c7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -31,86 +31,109 @@ ICESat-2 datasets to enable scientific discovery. To further enhance data discovery, we have developed the QUEST module to facilitate querying of ICESat-2 data and complimentary Argo oceanographic data, with additional dataset support expected in the future. -.. panels:: - :card: + intro-card text-center - :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 - :img-top-cls: pl-2 pr-2 pt-2 pb-2 +.. grid:: 1 2 2 3 + :gutter: 3 + :class-container: sd-text-center - --- - :img-top: https://cdn-icons-png.flaticon.com/128/2498/2498074.png + .. grid-item-card:: + :img-top: https://cdn-icons-png.flaticon.com/128/2498/2498074.png + :class-img-top: sd-p-2 + :class-card: sd-shadow-md - **Getting Started** - ^^^^^^^^^^^^^^^^^^^ + **Getting Started** + ^^^^^^^^^^^^^^^^^^^ - New to ICESat-2 or icepyx? - Learn how to install icepyx and use it to jumpstart your project today. - Check out our gallery of examples, too! + New to ICESat-2 or icepyx? + Learn how to install icepyx and use it to jumpstart your project today. + Check out our gallery of examples, too! - .. link-button:: install_ref - :type: ref - :text: Installation Instructions - :classes: stretched-link btn-outline-primary btn-block + .. button-ref:: install_ref + :ref-type: ref + :color: primary + :outline: + :expand: - --- - :img-top: https://cdn-icons-png.flaticon.com/128/3730/3730041.png + Installation Instructions - **User Guide** - ^^^^^^^^^^^^^^ + .. grid-item-card:: + :img-top: https://cdn-icons-png.flaticon.com/128/3730/3730041.png + :class-img-top: sd-p-2 + :class-card: sd-shadow-md - The user guide provides in-depth information on the tools and functionality - available for obtaining and interacting with ICESat-2 data products. + **User Guide** + ^^^^^^^^^^^^^^ - .. link-button:: api_doc_ref - :type: ref - :text: Software Docs - :classes: stretched-link btn-outline-primary btn-block + The user guide provides in-depth information on the tools and functionality + available for obtaining and interacting with ICESat-2 data products. - --- - :img-top: https://cdn-icons-png.flaticon.com/512/4230/4230997.png + .. button-ref:: api_doc_ref + :ref-type: ref + :color: primary + :outline: + :expand: - **Development Guide** - ^^^^^^^^^^^^^^^^^^^^^ + Software Docs - Have an idea or an ancillary dataset to contribute to icepyx? Go here for information on best practices - for developing and contributing to icepyx. + .. grid-item-card:: + :img-top: https://cdn-icons-png.flaticon.com/512/4230/4230997.png + :class-img-top: sd-p-2 + :class-card: sd-shadow-md - .. link-button:: dev_guide_label - :type: ref - :text: Development Guide - :classes: stretched-link btn-outline-primary btn-block + **Development Guide** + ^^^^^^^^^^^^^^^^^^^^^ - --- - :img-top: https://cdn-icons-png.flaticon.com/128/1283/1283342.png + Have an idea or an ancillary dataset to contribute to icepyx? Go here for information on best practices + for developing and contributing to icepyx. - **Get in Touch** - ^^^^^^^^^^^^^^^^ + .. button-ref:: dev_guide_label + :ref-type: ref + :color: primary + :outline: + :expand: - icepyx is more than just software! - We're a community of data producers, managers, and users - who collaborate openly and share code and skills - for every step along the entire data pipeline. Find resources for - your questions here! + Development Guide - .. link-button:: contact_ref_label - :type: ref - :text: Get Involved! - :classes: stretched-link btn-outline-primary btn-block + .. grid-item-card:: + :img-top: https://cdn-icons-png.flaticon.com/128/1283/1283342.png + :class-img-top: sd-p-2 + :class-card: sd-shadow-md - --- - :img-top: https://icesat-2.gsfc.nasa.gov/sites/default/files/MissionLogo_0.png - :img-top-cls: pl-2 pr-2 pt-4 pb-4 + **Get in Touch** + ^^^^^^^^^^^^^^^^ - **ICESat-2 Resources** - ^^^^^^^^^^^^^^^^^^^^^^ + icepyx is more than just software! + We're a community of data producers, managers, and users + who collaborate openly and share code and skills + for every step along the entire data pipeline. Find resources for + your questions here! - Curious about other tools for working with ICESat-2 data? - Want to share your resource? - Check out the amazing work already in progress! + .. button-ref:: contact_ref_label + :ref-type: ref + :color: primary + :outline: + :expand: - .. link-button:: resource_ref_label - :type: ref - :text: ICESat-2 Resource Guide - :classes: stretched-link btn-outline-primary btn-block + Get Involved! + + .. grid-item-card:: + :img-top: https://icesat-2.gsfc.nasa.gov/sites/default/files/MissionLogo_0.png + :class-img-top: sd-p-2 + :class-card: sd-shadow-md + + **ICESat-2 Resources** + ^^^^^^^^^^^^^^^^^^^^^^ + + Curious about other tools for working with ICESat-2 data? + Want to share your resource? + Check out the amazing work already in progress! + + .. button-ref:: resource_ref_label + :ref-type: ref + :color: primary + :outline: + :expand: + + ICESat-2 Resource Guide .. toctree:: diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index 9084ffd70..223dbde8d 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -4,11 +4,11 @@ - - + + classes_dev_uml - + icepyx.quest.dataset_scripts.argo.Argo @@ -37,34 +37,34 @@ search_data(params, presRange, printURL): str - + icepyx.quest.dataset_scripts.dataset.DataSet - -DataSet - - -__init__ -(spatial_extent, date_range, start_time, end_time) -_fmt_coordinates -() -_fmt_timerange -() -_validate_inputs -() -download -() -save -(filepath) -search_data -() -visualize -() + +DataSet + + +__init__ +(spatial_extent, date_range, start_time, end_time) +_fmt_coordinates +() +_fmt_timerange +() +_validate_inputs +() +download +() +save +(filepath) +search_data +() +visualize +() - + icepyx.quest.dataset_scripts.argo.Argo->icepyx.quest.dataset_scripts.dataset.DataSet - - + + @@ -75,402 +75,552 @@ - + + +icepyx.core.types.CMRParamsWithBbox + +CMRParamsWithBbox + +bounding_box : str + + + + +icepyx.core.types.CMRParamsWithPolygon + +CMRParamsWithPolygon + +polygon : str + + + + + icepyx.core.exceptions.DeprecationError - -DeprecationError - - - + +DeprecationError + + + + + + +icepyx.core.types.EGIParamsSubsetBase + +EGIParamsSubsetBase + +Coverage : NotRequired[str] +format : NotRequired[str] +projection : NotRequired[str] +projection_parameters : NotRequired[str] +time : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBbox + +EGIParamsSubsetBbox + +bbox : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBbox->icepyx.core.types.EGIParamsSubsetBase + + + + + +icepyx.core.types.EGIParamsSubsetBoundingShape + +EGIParamsSubsetBoundingShape + +Boundingshape : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBoundingShape->icepyx.core.types.EGIParamsSubsetBase + + + + + +icepyx.core.types.EGIRequiredParamsBase + +EGIRequiredParamsBase + +page_num : int +page_size : int +short_name : Literal +version : str + + + + + +icepyx.core.types.EGIRequiredParamsDownload + +EGIRequiredParamsDownload + +client_string : Literal['icepyx'] +include_meta : Literal['Y', 'N'] +request_mode : Literal['sync', 'async', 'stream'] + + + + + +icepyx.core.types.EGIRequiredParamsDownload->icepyx.core.types.EGIRequiredParamsBase + + + + + +icepyx.core.types.EGIRequiredParamsSearch + +EGIRequiredParamsSearch + + + + + + +icepyx.core.types.EGIRequiredParamsSearch->icepyx.core.types.EGIRequiredParamsBase + + - + icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -_auth : NoneType -_s3_initial_ts : NoneType, datetime -_s3login_credentials : NoneType -_session : NoneType -auth -s3login_credentials -session - -__init__(auth) -__str__() -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +_auth : NoneType +_s3_initial_ts : NoneType, datetime +_s3login_credentials : NoneType +_session : NoneType +auth +s3login_credentials +session + +__init__(auth) +__str__(): str +earthdata_login(uid, email, s3token): None + + + +icepyx.core.exceptions.ExhaustiveTypeGuardException + +ExhaustiveTypeGuardException + + + + + + +icepyx.core.exceptions.TypeGuardException + +TypeGuardException + + +__str__() + + + +icepyx.core.exceptions.ExhaustiveTypeGuardException->icepyx.core.exceptions.TypeGuardException + + - + icepyx.core.query.GenQuery - -GenQuery - -_spatial -_temporal -dates -end_time -spatial -spatial_extent -start_time -temporal - -__init__(spatial_extent, date_range, start_time, end_time) -__str__() + +GenQuery + +_spatial +_temporal +dates +end_time +spatial +spatial_extent +start_time +temporal + +__init__(spatial_extent, date_range, start_time, end_time) +__str__() - + icepyx.core.granules.Granules - -Granules - -avail : list -orderIDs : list - -__init__() -download(verbose, path, restart) -get_avail(CMRparams, reqparams, cloud) -place_order(CMRparams, reqparams, subsetparams, verbose, subset, geom_filepath) + +Granules + +avail : list +orderIDs : list + +__init__() +download(verbose, path, restart) +get_avail(CMRparams: CMRParams, reqparams: EGIRequiredParamsSearch, cloud: bool) +place_order(CMRparams: CMRParams, reqparams: EGIRequiredParamsDownload, subsetparams, verbose, subset, geom_filepath) - + icepyx.core.granules.Granules->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.query.Query - -Query - -CMRparams -_CMRparams -_about_product -_cust_options : dict -_cycles : list -_granules -_order_vars -_prod : NoneType, str -_readable_granule_name : list -_reqparams -_subsetparams : NoneType -_tracks : list -_version -cycles -dataset -granules -order_vars -product -product_version -reqparams -tracks - -__init__(product, spatial_extent, date_range, start_time, end_time, version, cycles, tracks, auth) -__str__() -avail_granules(ids, cycles, tracks, cloud) -download_granules(path, verbose, subset, restart) -latest_version() -order_granules(verbose, subset, email) -product_all_info() -product_summary_info() -show_custom_options(dictview) -subsetparams() -visualize_elevation() -visualize_spatial_extent() + +Query + +CMRparams +_CMRparams +_about_product +_cust_options : dict +_cycles : list +_granules +_order_vars +_prod : NoneType, str +_readable_granule_name : list +_reqparams +_subsetparams : Optional[apifmt.SubsetParameters] +_tracks : list +_version +cycles +dataset +granules +order_vars +product +product_version +reqparams +tracks + +__init__(product, spatial_extent, date_range, start_time, end_time, version, cycles, tracks, auth) +__str__() +avail_granules(ids, cycles, tracks, cloud) +download_granules(path, verbose, subset, restart) +latest_version() +order_granules(verbose, subset, email) +product_all_info() +product_summary_info() +show_custom_options(dictview) +subsetparams(): Union[EGIParamsSubset, dict[Never, Never]] +visualize_elevation() +visualize_spatial_extent() - + icepyx.core.granules.Granules->icepyx.core.query.Query - - -_granules + + +_granules - + icepyx.core.icesat2data.Icesat2Data - -Icesat2Data - - -__init__() + +Icesat2Data + + +__init__() - + icepyx.core.exceptions.NsidcQueryError - -NsidcQueryError - -errmsg -msgtxt : str - -__init__(errmsg, msgtxt) -__str__() + +NsidcQueryError + +errmsg +msgtxt : str + +__init__(errmsg, msgtxt) +__str__() - + icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + - + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + - + icepyx.core.APIformatting.Parameters - -Parameters - -_fmted_keys : NoneType, dict -_poss_keys : dict -_reqtype : NoneType, str -fmted_keys -partype -poss_keys - -__init__(partype, values, reqtype) -_check_valid_keys() -_get_possible_keys() -build_params() -check_req_values() -check_values() + +Parameters + +_fmted_keys : NoneType, dict +_reqtype : Optional[Literal['search', 'download']] +fmted_keys +partype : T +poss_keys + +__init__(partype: T, values: Optional[dict], reqtype: Optional[Literal['search', 'download']]) +_check_valid_keys(): None +build_params(): None +check_req_values(): bool +check_values(): bool - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_CMRparams + + +_CMRparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_reqparams + + +_reqparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams - + icepyx.core.query.Query->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.query.Query->icepyx.core.query.GenQuery - - + + - + icepyx.quest.quest.Quest - -Quest - -datasets : dict - -__init__(spatial_extent, date_range, start_time, end_time, proj) -__str__() -add_argo(params, presRange): None -add_icesat2(product, start_time, end_time, version, cycles, tracks, files): None -download_all(path) -save_all(path) -search_all() + +Quest + +datasets : dict + +__init__(spatial_extent, date_range, start_time, end_time, proj) +__str__() +add_argo(params, presRange): None +add_icesat2(product, start_time, end_time, version, cycles, tracks, files): None +download_all(path) +save_all(path) +search_all() - + icepyx.quest.quest.Quest->icepyx.core.query.GenQuery - - + + - + icepyx.core.read.Read - -Read - -_filelist -_out_obj : Dataset -_product -_read_vars -filelist -is_s3 -product -vars - -__init__(data_source, glob_kwargs, out_obj_type, product, filename_pattern, catalog) -_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) -_build_dataset_template(file) -_build_single_file_dataset(file, groups_list) -_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) -_read_single_grp(file, grp_path) -load() + +Read + +_filelist +_out_obj : Dataset +_product +_read_vars +filelist +is_s3 +product +vars + +__init__(data_source, glob_kwargs, out_obj_type, product, filename_pattern, catalog) +_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) +_build_dataset_template(file) +_build_single_file_dataset(file, groups_list) +_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) +_read_single_grp(file, grp_path) +load() - + icepyx.core.read.Read->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.spatial.Spatial - -Spatial - -_ext_type : str -_gdf_spat : GeoDataFrame -_geom_file : NoneType -_spatial_ext -_xdateln -extent -extent_as_gdf -extent_file -extent_type - -__init__(spatial_extent) -__str__() -fmt_for_CMR() -fmt_for_EGI() + +Spatial + +_ext_type : str +_gdf_spat : GeoDataFrame +_geom_file : NoneType +_spatial_ext +_xdateln +extent +extent_as_gdf +extent_file +extent_type + +__init__(spatial_extent) +__str__() +fmt_for_CMR() +fmt_for_EGI() - + icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial - + icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial - + icepyx.core.temporal.Temporal - -Temporal - -_end : datetime -_start : datetime -end -start - -__init__(date_range, start_time, end_time) -__str__() + +Temporal + +_end : datetime +_start : datetime +end +start + +__init__(date_range: Union[list, dict], start_time: Union[str, dt.time, None], end_time: Union[str, dt.time, None]) +__str__(): str - + icepyx.core.temporal.Temporal->icepyx.core.query.GenQuery - - -_temporal + + +_temporal - + icepyx.core.variables.Variables - -Variables - -_avail : NoneType, list -_path : NoneType -_product : NoneType, str -_version -path -product -version -wanted : NoneType, dict - -__init__(vartype, path, product, version, avail, wanted, auth) -_check_valid_lists(vgrp, allpaths, var_list, beam_list, keyword_list) -_get_combined_list(beam_list, keyword_list) -_get_sum_varlist(var_list, all_vars, defaults) -_iter_paths(sum_varlist, req_vars, vgrp, beam_list, keyword_list) -_iter_vars(sum_varlist, req_vars, vgrp) -append(defaults, var_list, beam_list, keyword_list) -avail(options, internal) -parse_var_list(varlist, tiered, tiered_vars) -remove(all, var_list, beam_list, keyword_list) + +Variables + +_avail : NoneType, list +_path : NoneType +_product : NoneType, str +_version +path +product +version +wanted : NoneType, dict + +__init__(vartype, path, product, version, avail, wanted, auth) +_check_valid_lists(vgrp, allpaths, var_list, beam_list, keyword_list) +_get_combined_list(beam_list, keyword_list) +_get_sum_varlist(var_list, all_vars, defaults) +_iter_paths(sum_varlist, req_vars, vgrp, beam_list, keyword_list) +_iter_vars(sum_varlist, req_vars, vgrp) +append(defaults, var_list, beam_list, keyword_list) +avail(options, internal) +parse_var_list(varlist, tiered, tiered_vars) +remove(all, var_list, beam_list, keyword_list) - + icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars - + icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars - + icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars - + icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars - + icepyx.core.visualization.Visualize - -Visualize - -bbox : list -cycles : NoneType -date_range : NoneType -product : NoneType, str -tracks : NoneType - -__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) -generate_OA_parameters(): list -grid_bbox(binsize): list -make_request(base_url, payload) -parallel_request_OA(): da.array -query_icesat2_filelist(): tuple -request_OA_data(paras): da.array -viz_elevation(): (hv.DynamicMap, hv.Layout) + +Visualize + +bbox : list +cycles : NoneType +date_range : NoneType +product : NoneType, str +tracks : NoneType + +__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) +generate_OA_parameters(): list +grid_bbox(binsize): list +make_request(base_url, payload) +parallel_request_OA(): da.array +query_icesat2_filelist(): tuple +request_OA_data(paras): da.array +viz_elevation(): tuple[hv.DynamicMap, hv.Layout] + + + +icepyx.core.APIformatting._FmtedKeysDescriptor + +_FmtedKeysDescriptor + + +__get__(instance: 'Parameters[Literal["CMR"]]', owner: Any): CMRParams + + + +icepyx.core.APIformatting._FmtedKeysDescriptor->icepyx.core.APIformatting.Parameters + + +fmted_keys diff --git a/doc/source/user_guide/documentation/classes_user_uml.svg b/doc/source/user_guide/documentation/classes_user_uml.svg index 8b1273598..b9a3c2508 100644 --- a/doc/source/user_guide/documentation/classes_user_uml.svg +++ b/doc/source/user_guide/documentation/classes_user_uml.svg @@ -4,11 +4,11 @@ - - + + classes_user_uml - + icepyx.core.auth.AuthenticationError @@ -18,318 +18,470 @@ - + +icepyx.core.types.CMRParamsWithBbox + +CMRParamsWithBbox + +bounding_box : str + + + + + +icepyx.core.types.CMRParamsWithPolygon + +CMRParamsWithPolygon + +polygon : str + + + + + icepyx.core.exceptions.DeprecationError - -DeprecationError - - - + +DeprecationError + + + + + + +icepyx.core.types.EGIParamsSubsetBase + +EGIParamsSubsetBase + +Coverage : NotRequired[str] +format : NotRequired[str] +projection : NotRequired[str] +projection_parameters : NotRequired[str] +time : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBbox + +EGIParamsSubsetBbox + +bbox : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBbox->icepyx.core.types.EGIParamsSubsetBase + + + + + +icepyx.core.types.EGIParamsSubsetBoundingShape + +EGIParamsSubsetBoundingShape + +Boundingshape : NotRequired[str] + + + + + +icepyx.core.types.EGIParamsSubsetBoundingShape->icepyx.core.types.EGIParamsSubsetBase + + + + + +icepyx.core.types.EGIRequiredParamsBase + +EGIRequiredParamsBase + +page_num : int +page_size : int +short_name : Literal +version : str + + + + + +icepyx.core.types.EGIRequiredParamsDownload + +EGIRequiredParamsDownload + +client_string : Literal['icepyx'] +include_meta : Literal['Y', 'N'] +request_mode : Literal['sync', 'async', 'stream'] + + + + + +icepyx.core.types.EGIRequiredParamsDownload->icepyx.core.types.EGIRequiredParamsBase + + + + + +icepyx.core.types.EGIRequiredParamsSearch + +EGIRequiredParamsSearch + + + + + + +icepyx.core.types.EGIRequiredParamsSearch->icepyx.core.types.EGIRequiredParamsBase + + - + icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -auth -s3login_credentials -session - -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +auth +s3login_credentials +session + +earthdata_login(uid, email, s3token): None + + + +icepyx.core.exceptions.ExhaustiveTypeGuardException + +ExhaustiveTypeGuardException + + + + + + +icepyx.core.exceptions.TypeGuardException + +TypeGuardException + + + + + + +icepyx.core.exceptions.ExhaustiveTypeGuardException->icepyx.core.exceptions.TypeGuardException + + - + icepyx.core.query.GenQuery - -GenQuery - -dates -end_time -spatial -spatial_extent -start_time -temporal - - + +GenQuery + +dates +end_time +spatial +spatial_extent +start_time +temporal + + - + icepyx.core.granules.Granules - -Granules - -avail : list -orderIDs : list - -download(verbose, path, restart) -get_avail(CMRparams, reqparams, cloud) -place_order(CMRparams, reqparams, subsetparams, verbose, subset, geom_filepath) + +Granules + +avail : list +orderIDs : list + +download(verbose, path, restart) +get_avail(CMRparams: CMRParams, reqparams: EGIRequiredParamsSearch, cloud: bool) +place_order(CMRparams: CMRParams, reqparams: EGIRequiredParamsDownload, subsetparams, verbose, subset, geom_filepath) - + icepyx.core.granules.Granules->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.query.Query - -Query - -CMRparams -cycles -dataset -granules -order_vars -product -product_version -reqparams -tracks - -avail_granules(ids, cycles, tracks, cloud) -download_granules(path, verbose, subset, restart) -latest_version() -order_granules(verbose, subset, email) -product_all_info() -product_summary_info() -show_custom_options(dictview) -subsetparams() -visualize_elevation() -visualize_spatial_extent() + +Query + +CMRparams +cycles +dataset +granules +order_vars +product +product_version +reqparams +tracks + +avail_granules(ids, cycles, tracks, cloud) +download_granules(path, verbose, subset, restart) +latest_version() +order_granules(verbose, subset, email) +product_all_info() +product_summary_info() +show_custom_options(dictview) +subsetparams(): Union[EGIParamsSubset, dict[Never, Never]] +visualize_elevation() +visualize_spatial_extent() - + icepyx.core.granules.Granules->icepyx.core.query.Query - - -_granules + + +_granules - + icepyx.core.icesat2data.Icesat2Data - -Icesat2Data - - - + +Icesat2Data + + + - + icepyx.core.exceptions.NsidcQueryError - -NsidcQueryError - -errmsg -msgtxt : str - - + +NsidcQueryError + +errmsg +msgtxt : str + + - + icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + - + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + - + icepyx.core.APIformatting.Parameters - -Parameters - -fmted_keys -partype -poss_keys - -build_params() -check_req_values() -check_values() + +Parameters + +fmted_keys +partype : T +poss_keys + +build_params(): None +check_req_values(): bool +check_values(): bool - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_CMRparams + + +_CMRparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_reqparams + + +_reqparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams - + icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams - + icepyx.core.query.Query->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.query.Query->icepyx.core.query.GenQuery - - + + - + icepyx.core.read.Read - -Read - -filelist -is_s3 -product -vars - -load() + +Read + +filelist +is_s3 +product +vars + +load() - + icepyx.core.read.Read->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.spatial.Spatial - -Spatial - -extent -extent_as_gdf -extent_file -extent_type - -fmt_for_CMR() -fmt_for_EGI() + +Spatial + +extent +extent_as_gdf +extent_file +extent_type + +fmt_for_CMR() +fmt_for_EGI() - + icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial - + icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial - + icepyx.core.temporal.Temporal - -Temporal - -end -start - - + +Temporal + +end +start + + - + icepyx.core.temporal.Temporal->icepyx.core.query.GenQuery - - -_temporal + + +_temporal - + icepyx.core.variables.Variables - -Variables - -path -product -version -wanted : NoneType, dict - -append(defaults, var_list, beam_list, keyword_list) -avail(options, internal) -parse_var_list(varlist, tiered, tiered_vars) -remove(all, var_list, beam_list, keyword_list) + +Variables + +path +product +version +wanted : NoneType, dict + +append(defaults, var_list, beam_list, keyword_list) +avail(options, internal) +parse_var_list(varlist, tiered, tiered_vars) +remove(all, var_list, beam_list, keyword_list) - + icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - - + + - + icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars - + icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars - + icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars - + icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars - + icepyx.core.visualization.Visualize - -Visualize - -bbox : list -cycles : NoneType -date_range : NoneType -product : NoneType, str -tracks : NoneType - -generate_OA_parameters(): list -grid_bbox(binsize): list -make_request(base_url, payload) -parallel_request_OA(): da.array -query_icesat2_filelist(): tuple -request_OA_data(paras): da.array -viz_elevation(): (hv.DynamicMap, hv.Layout) + +Visualize + +bbox : list +cycles : NoneType +date_range : NoneType +product : NoneType, str +tracks : NoneType + +generate_OA_parameters(): list +grid_bbox(binsize): list +make_request(base_url, payload) +parallel_request_OA(): da.array +query_icesat2_filelist(): tuple +request_OA_data(paras): da.array +viz_elevation(): tuple[hv.DynamicMap, hv.Layout] + + + +icepyx.core.APIformatting._FmtedKeysDescriptor + +_FmtedKeysDescriptor + + + + + + +icepyx.core.APIformatting._FmtedKeysDescriptor->icepyx.core.APIformatting.Parameters + + +fmted_keys diff --git a/doc/source/user_guide/documentation/icepyx.rst b/doc/source/user_guide/documentation/icepyx.rst index eec823e10..bb71b63e0 100644 --- a/doc/source/user_guide/documentation/icepyx.rst +++ b/doc/source/user_guide/documentation/icepyx.rst @@ -19,3 +19,4 @@ Diagrams are updated automatically after a pull request (PR) is approved and bef read variables components + types diff --git a/doc/source/user_guide/documentation/packages_user_uml.svg b/doc/source/user_guide/documentation/packages_user_uml.svg index 2cfe26a67..f05094a12 100644 --- a/doc/source/user_guide/documentation/packages_user_uml.svg +++ b/doc/source/user_guide/documentation/packages_user_uml.svg @@ -4,190 +4,220 @@ - + packages_user_uml - + icepyx.core - -icepyx.core + +icepyx.core icepyx.core.APIformatting - -icepyx.core.APIformatting - - - -icepyx.core.auth - -icepyx.core.auth + +icepyx.core.APIformatting icepyx.core.exceptions - -icepyx.core.exceptions + +icepyx.core.exceptions - + +icepyx.core.APIformatting->icepyx.core.exceptions + + + + + +icepyx.core.types + +icepyx.core.types + + + +icepyx.core.APIformatting->icepyx.core.types + + + + + +icepyx.core.auth + +icepyx.core.auth + + + icepyx.core.auth->icepyx.core.exceptions - - + + icepyx.core.granules - -icepyx.core.granules + +icepyx.core.granules - + icepyx.core.granules->icepyx.core.auth - - + + + + + +icepyx.core.granules->icepyx.core.types + + - + icepyx.core.urls - -icepyx.core.urls + +icepyx.core.urls - + icepyx.core.granules->icepyx.core.urls - - + + icepyx.core.icesat2data - -icepyx.core.icesat2data + +icepyx.core.icesat2data - + icepyx.core.icesat2data->icepyx.core.exceptions - - + + icepyx.core.is2ref - -icepyx.core.is2ref + +icepyx.core.is2ref - + icepyx.core.is2ref->icepyx.core.urls - - + + icepyx.core.query - -icepyx.core.query + +icepyx.core.query - + icepyx.core.query->icepyx.core.auth - - + + - + icepyx.core.query->icepyx.core.exceptions - - + + - + icepyx.core.query->icepyx.core.granules - - + + + + + +icepyx.core.query->icepyx.core.types + + - + icepyx.core.variables - -icepyx.core.variables + +icepyx.core.variables - + icepyx.core.query->icepyx.core.variables - - + + - + icepyx.core.visualization - -icepyx.core.visualization + +icepyx.core.visualization - + icepyx.core.query->icepyx.core.visualization - - + + icepyx.core.read - -icepyx.core.read + +icepyx.core.read - + icepyx.core.read->icepyx.core.auth - - + + - + icepyx.core.read->icepyx.core.exceptions - - + + - + icepyx.core.read->icepyx.core.variables - - + + icepyx.core.spatial - -icepyx.core.spatial + +icepyx.core.spatial icepyx.core.temporal - -icepyx.core.temporal + +icepyx.core.temporal - + icepyx.core.validate_inputs - -icepyx.core.validate_inputs + +icepyx.core.validate_inputs - + icepyx.core.variables->icepyx.core.auth - - + + - + icepyx.core.variables->icepyx.core.exceptions - - + + diff --git a/doc/source/user_guide/documentation/types.rst b/doc/source/user_guide/documentation/types.rst new file mode 100644 index 000000000..991a80431 --- /dev/null +++ b/doc/source/user_guide/documentation/types.rst @@ -0,0 +1,11 @@ +Types +===== + +.. automodule:: icepyx.core.types + :members: + :undoc-members: + :exclude-members: CMRParamsBase,CMRParamsWithBbox,CMRParamsWithPolygon + +.. COMMENT. `exclude-members` specified above is required because those models + contain symbols ('[', ']') in some keys, which sphinx doesn't like. + See: https://github.com/sphinx-doc/sphinx/issues/11039 diff --git a/icepyx/core/APIformatting.py b/icepyx/core/APIformatting.py index 4b1966910..dc20c6878 100644 --- a/icepyx/core/APIformatting.py +++ b/icepyx/core/APIformatting.py @@ -1,6 +1,14 @@ -# Generate and format information for submitting to API (CMR and NSIDC) +"""Generate and format information for submitting to API (CMR and NSIDC).""" import datetime as dt +from typing import Any, Generic, Literal, Optional, TypeVar, Union, overload + +from icepyx.core.exceptions import ExhaustiveTypeGuardException, TypeGuardException +from icepyx.core.types import ( + CMRParams, + EGIParamsSubset, + EGIRequiredParams, +) # ---------------------------------------------------------------------- # parameter-specific formatting for display @@ -29,10 +37,6 @@ def _fmt_temporal(start, end, key): assert isinstance(start, dt.datetime) assert isinstance(end, dt.datetime) - assert key in [ - "time", - "temporal", - ], "An invalid time key was submitted for formatting." if key == "temporal": fmt_timerange = ( @@ -46,6 +50,8 @@ def _fmt_temporal(start, end, key): + "," + end.strftime("%Y-%m-%dT%H:%M:%S") ) + else: + raise ValueError("An invalid time key was submitted for formatting.") return {key: fmt_timerange} @@ -183,12 +189,56 @@ def to_string(params): return "&".join(param_list) +ParameterType = Literal["CMR", "required", "subset"] +# DevGoal: When Python 3.12 is minimum supported version, migrate to PEP695 style +T = TypeVar("T", bound=ParameterType) + + +class _FmtedKeysDescriptor: + """Enable the Parameters class' fmted_keys property to be typechecked correctly. + + See: https://github.com/microsoft/pyright/issues/3071#issuecomment-1043978070 + """ + + @overload + def __get__( + self, + instance: 'Parameters[Literal["CMR"]]', + owner: Any, + ) -> CMRParams: ... + + @overload + def __get__( + self, + instance: 'Parameters[Literal["required"]]', + owner: Any, + ) -> EGIRequiredParams: ... + + @overload + def __get__( + self, + instance: 'Parameters[Literal["subset"]]', + owner: Any, + ) -> EGIParamsSubset: ... + + def __get__( + self, + instance: "Parameters", + owner: Any, + ) -> Union[CMRParams, EGIRequiredParams, EGIParamsSubset]: + """ + Returns the dictionary of formatted keys associated with the + parameter object. + """ + return instance._fmted_keys # pyright: ignore[reportReturnType] + + # ---------------------------------------------------------------------- # DevNote: Currently, this class is not tested!! # DevGoal: this could be expanded, similar to the variables class, to provide users with valid options if need be # DevGoal: currently this does not do much by way of checking/formatting of other subsetting options (reprojection or formats) # it would be great to incorporate that so that people can't just feed any keywords in... -class Parameters: +class Parameters(Generic[T]): """ Build and update the parameter lists needed to submit a data order @@ -206,7 +256,17 @@ class Parameters: on the type of query. Must be one of ['search','download'] """ - def __init__(self, partype, values=None, reqtype=None): + partype: T + _reqtype: Optional[Literal["search", "download"]] + fmted_keys = _FmtedKeysDescriptor() + # _fmted_keys: Union[CMRParams, EGISpecificRequiredParams, EGIParamsSubset] + + def __init__( + self, + partype: T, + values: Optional[dict] = None, + reqtype: Optional[Literal["search", "download"]] = None, + ): assert partype in [ "CMR", "required", @@ -224,39 +284,14 @@ def __init__(self, partype, values=None, reqtype=None): self._fmted_keys = values if values is not None else {} @property - def poss_keys(self): + def poss_keys(self) -> dict[str, list[str]]: """ Returns a list of possible input keys for the given parameter object. Possible input keys depend on the parameter type (partype). """ - if not hasattr(self, "_poss_keys"): - self._get_possible_keys() - - return self._poss_keys - - # @property - # def wanted_keys(self): - # if not hasattr(_wanted): - # self._wanted = [] - - # return self._wanted - - @property - def fmted_keys(self): - """ - Returns the dictionary of formatted keys associated with the - parameter object. - """ - return self._fmted_keys - - def _get_possible_keys(self): - """ - Use the parameter type to get a list of possible parameter keys. - """ - if self.partype == "CMR": - self._poss_keys = { + return { "spatial": ["bounding_box", "polygon"], "optional": [ "temporal", @@ -266,7 +301,7 @@ def _get_possible_keys(self): ], } elif self.partype == "required": - self._poss_keys = { + return { "search": ["short_name", "version", "page_size"], "download": [ "short_name", @@ -281,7 +316,7 @@ def _get_possible_keys(self): ], } elif self.partype == "subset": - self._poss_keys = { + return { "spatial": ["bbox", "Boundingshape"], "optional": [ "time", @@ -291,8 +326,17 @@ def _get_possible_keys(self): "Coverage", ], } + else: + raise ExhaustiveTypeGuardException + + # @property + # def wanted_keys(self): + # if not hasattr(_wanted): + # self._wanted = [] - def _check_valid_keys(self): + # return self._wanted + + def _check_valid_keys(self) -> None: """ Checks that any keys passed in with values are valid keys. """ @@ -302,13 +346,13 @@ def _check_valid_keys(self): val_list = list({val for lis in self.poss_keys.values() for val in lis}) - for key in self.fmted_keys: + for key in self.fmted_keys: # pyright: ignore[reportAttributeAccessIssue] assert key in val_list, ( "An invalid key (" + key + ") was passed. Please remove it using `del`" ) # DevNote: can check_req_values and check_values be combined? - def check_req_values(self): + def check_req_values(self) -> bool: """ Check that all of the required keys have values, if the key was passed in with the values parameter. @@ -317,17 +361,22 @@ def check_req_values(self): assert ( self.partype == "required" ), "You cannot call this function for your parameter type" + + if not self._reqtype: + raise TypeGuardException + reqkeys = self.poss_keys[self._reqtype] - if all(keys in self.fmted_keys for keys in reqkeys): + if all(keys in self.fmted_keys for keys in reqkeys): # pyright: ignore[reportAttributeAccessIssue] assert all( - self.fmted_keys.get(key, -9999) != -9999 for key in reqkeys + self.fmted_keys.get(key, -9999) != -9999 # pyright: ignore[reportAttributeAccessIssue] + for key in reqkeys ), "One of your formatted parameters is missing a value" return True else: return False - def check_values(self): + def check_values(self) -> bool: """ Check that the non-required keys have values, if the key was passed in with the values parameter. @@ -341,13 +390,14 @@ def check_values(self): # not the most robust check, but better than nothing... if any(keys in self._fmted_keys for keys in spatial_keys): assert any( - self.fmted_keys.get(key, -9999) != -9999 for key in spatial_keys + self.fmted_keys.get(key, -9999) != -9999 # pyright: ignore[reportAttributeAccessIssue] + for key in spatial_keys ), "One of your formatted parameters is missing a value" return True else: return False - def build_params(self, **kwargs): + def build_params(self, **kwargs) -> None: """ Build the parameter dictionary of formatted key:value pairs for submission to NSIDC in the data request. @@ -377,6 +427,9 @@ def build_params(self, **kwargs): self._check_valid_keys() if self.partype == "required": + if not self._reqtype: + raise TypeGuardException + if self.check_req_values() and kwargs == {}: pass else: @@ -395,8 +448,6 @@ def build_params(self, **kwargs): self._fmted_keys.update({key: kwargs[key]}) except KeyError: self._fmted_keys.update({key: kwargs["product"]}) - elif key == "version": - self._fmted_keys.update({key: kwargs["version"]}) elif key in kwargs: self._fmted_keys.update({key: kwargs[key]}) elif key in defaults: @@ -434,6 +485,7 @@ def build_params(self, **kwargs): if any(keys in self._fmted_keys for keys in spatial_keys): pass else: + k = None if self.partype == "CMR": k = kwargs["extent_type"] elif self.partype == "subset": @@ -442,4 +494,12 @@ def build_params(self, **kwargs): elif kwargs["extent_type"] == "polygon": k = "Boundingshape" + if not k: + raise TypeGuardException + self._fmted_keys.update({k: kwargs["spatial_extent"]}) + + +CMRParameters = Parameters[Literal["CMR"]] +RequiredParameters = Parameters[Literal["required"]] +SubsetParameters = Parameters[Literal["subset"]] diff --git a/icepyx/core/auth.py b/icepyx/core/auth.py index 9f12fbecf..71b4393e5 100644 --- a/icepyx/core/auth.py +++ b/icepyx/core/auth.py @@ -54,7 +54,7 @@ def __init__(self, auth=None): self._s3login_credentials = None self._s3_initial_ts = None # timer for 1h expiration on s3 credentials - def __str__(self): + def __str__(self) -> str: if self.session: repr_string = "EarthdataAuth obj with session initialized" else: diff --git a/icepyx/core/exceptions.py b/icepyx/core/exceptions.py index 94bbea768..085fed8c9 100644 --- a/icepyx/core/exceptions.py +++ b/icepyx/core/exceptions.py @@ -1,3 +1,10 @@ +ISSUE_REPORTING_INSTRUCTIONS = ( + "If you are a user seeing this message, the developers of this software have made a" + " mistake! Please report the full error traceback in the icepyx GitHub repository:" + " " +) + + class DeprecationError(Exception): """ Class raised for use of functionality that is no longer supported by icepyx. @@ -24,3 +31,25 @@ def __init__( def __str__(self): return f"{self.msgtxt}: {self.errmsg}" + + +class TypeGuardException(Exception): + """ + Should never be raised at runtime. + + Used in cases where a runtime check is not desired, but we want to add a "type guard" + (https://github.com/microsoft/pyright/blob/main/docs/type-concepts-advanced.md#type-guards) + to give the type checker more information. + """ + + def __str__(self): + return ISSUE_REPORTING_INSTRUCTIONS + + +class ExhaustiveTypeGuardException(TypeGuardException): + """ + Should never be raised at runtime. + + Used exclusively in cases where the typechecker needs a typeguard to tell it that a + check is exhaustive. + """ diff --git a/icepyx/core/granules.py b/icepyx/core/granules.py index b29a147e1..5512120bb 100644 --- a/icepyx/core/granules.py +++ b/icepyx/core/granules.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import io import json @@ -10,10 +12,16 @@ import numpy as np import requests +from requests.compat import unquote import icepyx.core.APIformatting as apifmt from icepyx.core.auth import EarthdataAuthMixin import icepyx.core.exceptions +from icepyx.core.types import ( + CMRParams, + EGIRequiredParamsDownload, + EGIRequiredParamsSearch, +) from icepyx.core.urls import DOWNLOAD_BASE_URL, GRANULE_SEARCH_BASE_URL, ORDER_BASE_URL @@ -170,24 +178,32 @@ def __init__( # ---------------------------------------------------------------------- # Methods - def get_avail(self, CMRparams, reqparams, cloud=False): + def get_avail( + self, + CMRparams: CMRParams, + reqparams: EGIRequiredParamsSearch, + cloud: bool = False, + ): """ Get a list of available granules for the query object's parameters. Generates the `avail` attribute of the granules object. Parameters ---------- - CMRparams : dictionary + CMRparams : Dictionary of properly formatted CMR search parameters. - reqparams : dictionary + reqparams : Dictionary of properly formatted parameters required for searching, ordering, or downloading from NSIDC. - cloud : deprecated, boolean, default False + cloud : CMR metadata is always collected for the cloud system. + .. deprecated:: 1.2 + This parameter is ignored. + Notes ----- - This function is used by query.Query.avail_granules(), which automatically + This function is used by ``query.Query.avail_granules()``, which automatically feeds in the required parameters. See Also @@ -261,13 +277,13 @@ def get_avail(self, CMRparams, reqparams, cloud=False): # DevGoal: add kwargs to allow subsetting and more control over request options. def place_order( self, - CMRparams, - reqparams, + CMRparams: CMRParams, + reqparams: EGIRequiredParamsDownload, subsetparams, verbose, subset=True, geom_filepath=None, - ): # , **kwargs): + ): """ Place an order for the available granules for the query object. Adds the list of zipped files (orders) to the granules data object (which is @@ -276,11 +292,11 @@ def place_order( Parameters ---------- - CMRparams : dictionary + CMRparams : Dictionary of properly formatted CMR search parameters. - reqparams : dictionary + reqparams : Dictionary of properly formatted parameters required for searching, ordering, - or downloading from NSIDC. + or downloading from NSIDC (via their EGI system). subsetparams : dictionary Dictionary of properly formatted subsetting parameters. An empty dictionary is passed as input here when subsetting is set to False in query methods. @@ -359,7 +375,7 @@ def place_order( request.raise_for_status() esir_root = ET.fromstring(request.content) if verbose is True: - print("Order request URL: ", requests.utils.unquote(request.url)) + print("Order request URL: ", unquote(request.url)) print( "Order request response XML content: ", request.content.decode("utf-8"), @@ -396,6 +412,7 @@ def place_order( status = statuslist[0] print("Initial status of your order request at NSIDC is: ", status) + loop_root = None # If status is already finished without going into pending/processing if status.startswith("complete"): loop_response = self.session.get(statusURL) @@ -425,6 +442,12 @@ def place_order( if status == "pending" or status == "processing": continue + if not isinstance(loop_root, ET.Element): + # The typechecker needs help knowing that at this point loop_root is + # set, as it can't tell that the conditionals above are supposed to be + # exhaustive. + raise icepyx.core.exceptions.ExhaustiveTypeGuardException + # Order can either complete, complete_with_errors, or fail: # Provide complete_with_errors error message: if status == "complete_with_errors" or status == "failed": diff --git a/icepyx/core/query.py b/icepyx/core/query.py index d547a959f..763ec6c52 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -1,7 +1,9 @@ import pprint +from typing import Optional, Union, cast import geopandas as gpd import matplotlib.pyplot as plt +from typing_extensions import Never import icepyx.core.APIformatting as apifmt from icepyx.core.auth import EarthdataAuthMixin @@ -11,6 +13,12 @@ import icepyx.core.is2ref as is2ref import icepyx.core.spatial as spat import icepyx.core.temporal as tp +from icepyx.core.types import ( + CMRParams, + EGIParamsSubset, + EGIRequiredParams, + EGIRequiredParamsDownload, +) import icepyx.core.validate_inputs as val from icepyx.core.variables import Variables as Variables from icepyx.core.visualization import Visualize @@ -118,6 +126,8 @@ class GenQuery: Quest """ + _temporal: tp.Temporal + def __init__( self, spatial_extent=None, @@ -149,7 +159,7 @@ def __str__(self): # Properties @property - def temporal(self): + def temporal(self) -> Union[tp.Temporal, list[str]]: """ Return the Temporal object containing date/time range information for the query object. @@ -246,7 +256,7 @@ def spatial_extent(self): return (self._spatial._ext_type, self._spatial._spatial_ext) @property - def dates(self): + def dates(self) -> list[str]: """ Return an array showing the date range of the query object. Dates are returned as an array containing the start and end datetime @@ -271,7 +281,7 @@ def dates(self): ] # could also use self._start.date() @property - def start_time(self): + def start_time(self) -> Union[list[str], str]: """ Return the start time specified for the start date. @@ -295,7 +305,7 @@ def start_time(self): return self._temporal._start.strftime("%H:%M:%S") @property - def end_time(self): + def end_time(self) -> Union[list[str], str]: """ Return the end time specified for the end date. @@ -393,6 +403,10 @@ class Query(GenQuery, EarthdataAuthMixin): GenQuery """ + _CMRparams: apifmt.CMRParameters + _reqparams: apifmt.RequiredParameters + _subsetparams: Optional[apifmt.SubsetParameters] + # ---------------------------------------------------------------------- # Constructors @@ -532,7 +546,7 @@ def tracks(self): return sorted(set(self._tracks)) @property - def CMRparams(self): + def CMRparams(self) -> CMRParams: """ Display the CMR key:value pairs that will be submitted. It generates the dictionary if it does not already exist. @@ -573,7 +587,7 @@ def CMRparams(self): return self._CMRparams.fmted_keys @property - def reqparams(self): + def reqparams(self) -> EGIRequiredParams: """ Display the required key:value pairs that will be submitted. It generates the dictionary if it does not already exist. @@ -599,7 +613,7 @@ def reqparams(self): # @property # DevQuestion: if I make this a property, I get a "dict" object is not callable # when I try to give input kwargs... what approach should I be taking? - def subsetparams(self, **kwargs): + def subsetparams(self, **kwargs) -> Union[EGIParamsSubset, dict[Never, Never]]: """ Display the subsetting key:value pairs that will be submitted. It generates the dictionary if it does not already exist @@ -1001,7 +1015,7 @@ def order_granules(self, verbose=False, subset=True, email=False, **kwargs): if "email" in self._reqparams.fmted_keys or email is False: self._reqparams.build_params(**self._reqparams.fmted_keys) elif email is True: - user_profile = self.auth.get_user_profile() + user_profile = self.auth.get_user_profile() # pyright: ignore[reportAttributeAccessIssue] self._reqparams.build_params( **self._reqparams.fmted_keys, email=user_profile["email_address"] ) @@ -1032,7 +1046,7 @@ def order_granules(self, verbose=False, subset=True, email=False, **kwargs): tempCMRparams["readable_granule_name[]"] = gran self._granules.place_order( tempCMRparams, - self.reqparams, + cast(EGIRequiredParamsDownload, self.reqparams), self.subsetparams(**kwargs), verbose, subset, @@ -1042,7 +1056,7 @@ def order_granules(self, verbose=False, subset=True, email=False, **kwargs): else: self._granules.place_order( self.CMRparams, - self.reqparams, + cast(EGIRequiredParamsDownload, self.reqparams), self.subsetparams(**kwargs), verbose, subset, @@ -1135,14 +1149,14 @@ def visualize_spatial_extent( import geoviews as gv from shapely.geometry import Polygon # noqa: F401 - gv.extension("bokeh") + gv.extension("bokeh") # pyright: ignore[reportCallIssue] bbox_poly = gv.Path(gdf["geometry"]).opts(color="red", line_color="red") tile = gv.tile_sources.EsriImagery.opts(width=500, height=500) - return tile * bbox_poly + return tile * bbox_poly # pyright: ignore[reportOperatorIssue] except ImportError: - world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) # pyright: ignore[reportAttributeAccessIssue] f, ax = plt.subplots(1, figsize=(12, 6)) world.plot(ax=ax, facecolor="lightgray", edgecolor="gray") gdf.plot(ax=ax, color="#FF8C00", alpha=0.7) diff --git a/icepyx/core/temporal.py b/icepyx/core/temporal.py index c02326fbf..86b7a56d1 100644 --- a/icepyx/core/temporal.py +++ b/icepyx/core/temporal.py @@ -1,12 +1,11 @@ import datetime as dt +from typing import Union import warnings -""" -Helper functions for validation of dates -""" +#### Helper functions for validation of dates #### -def convert_string_to_date(date): +def convert_string_to_date(date: str) -> dt.date: """ Converts a string to a datetime object. Throws an error if an invalid format is passed in. @@ -49,7 +48,7 @@ def convert_string_to_date(date): ) -def check_valid_date_range(start, end): +def check_valid_date_range(start: dt.date, end: dt.date) -> None: """ Helper function for checking if a date range is valid. @@ -86,7 +85,10 @@ def check_valid_date_range(start, end): ), "Your date range is invalid; end date MUST be on or after the start date." -def validate_times(start_time, end_time): +def validate_times( + start_time: Union[str, dt.time, None], + end_time: Union[str, dt.time, None], +) -> tuple[dt.time, dt.time]: """ Validates the start and end times passed into __init__ and returns them as datetime.time objects. @@ -141,7 +143,11 @@ def validate_times(start_time, end_time): return start_time, end_time -def validate_date_range_datestr(date_range, start_time=None, end_time=None): +def validate_date_range_datestr( + date_range: list[str], + start_time: Union[str, dt.time, None] = None, + end_time: Union[str, dt.time, None] = None, +) -> tuple[dt.datetime, dt.datetime]: """ Validates a date range provided in the form of a list of strings. @@ -150,12 +156,10 @@ def validate_date_range_datestr(date_range, start_time=None, end_time=None): Parameters ---------- - date_range: list(str) + date_range: A date range provided in the form of a list of strings Strings must be of formats accepted by validate_inputs_temporal.convert_string_to_date(). List must be of length 2. - start_time: string, datetime.time, None - end_time: string, datetime.time, None Returns ------- @@ -185,17 +189,19 @@ def validate_date_range_datestr(date_range, start_time=None, end_time=None): return _start, _end -def validate_date_range_datetime(date_range, start_time=None, end_time=None): +def validate_date_range_datetime( + date_range: list[dt.datetime], + start_time: Union[str, dt.time, None] = None, + end_time: Union[str, dt.time, None] = None, +) -> tuple[dt.datetime, dt.datetime]: """ Validates a date range provided in the form of a list of datetimes. Parameters ---------- - date_range: list(datetime.datetime) + date_range: A date range provided in the form of a list of datetimes. List must be of length 2. - start_time: None, string, datetime.time - end_time: None, string, datetime.time NOTE: If start and/or end times are given, they will be **ignored** in favor of the time from the start/end datetime.datetime objects. @@ -224,7 +230,11 @@ def validate_date_range_datetime(date_range, start_time=None, end_time=None): return date_range[0], date_range[1] -def validate_date_range_date(date_range, start_time=None, end_time=None): +def validate_date_range_date( + date_range: list[dt.date], + start_time: Union[str, dt.time, None] = None, + end_time: Union[str, dt.time, None] = None, +): """ Validates a date range provided in the form of a list of datetime.date objects. @@ -233,11 +243,9 @@ def validate_date_range_date(date_range, start_time=None, end_time=None): Parameters ---------- - date_range: list(str) + date_range: A date range provided in the form of a list of datetime.dates. List must be of length 2. - start_time: string or datetime.time - end_time: string or datetime.time Returns ------- @@ -261,14 +269,18 @@ def validate_date_range_date(date_range, start_time=None, end_time=None): return _start, _end -def validate_date_range_dict(date_range, start_time=None, end_time=None): +def validate_date_range_dict( + date_range: dict[str, Union[str, dt.date]], + start_time: Union[str, dt.time, None] = None, + end_time: Union[str, dt.time, None] = None, +) -> tuple[dt.datetime, dt.datetime]: """ Validates a date range provided in the form of a dict with the following keys: Parameters ---------- - date_range: dict(str, datetime.datetime, datetime.date) + date_range: A date range provided in the form of a dict. date_range must contain only the following keys: * `start_date`: start date, type can be of dt.datetime, dt.date, or string @@ -278,8 +290,6 @@ def validate_date_range_dict(date_range, start_time=None, end_time=None): If the values are of type dt.datetime and were created without times, the datetime package defaults of all 0s are used and the start_time/end_time parameters will be ignored! - start_time: string or datetime.time - end_time: string or datetime.time Returns @@ -293,7 +303,6 @@ def validate_date_range_dict(date_range, start_time=None, end_time=None): >>> valid_drange = validate_date_range_dict(drange, "00:00:00", "23:59:59") >>> valid_drange (datetime.datetime(2016, 1, 1, 0, 0), datetime.datetime(2020, 1, 1, 23, 59, 59)) - """ # Try to get keys from date_range dict @@ -366,7 +375,15 @@ def validate_date_range_dict(date_range, start_time=None, end_time=None): class Temporal: - def __init__(self, date_range, start_time=None, end_time=None): + _start: dt.datetime + _end: dt.datetime + + def __init__( + self, + date_range: Union[list, dict], + start_time: Union[str, dt.time, None] = None, + end_time: Union[str, dt.time, None] = None, + ): """ Validates input from "date_range" argument (and start/end time arguments, if provided), then creates a Temporal object with validated inputs as properties of the object. @@ -376,7 +393,7 @@ def __init__(self, date_range, start_time=None, end_time=None): Parameters ---------- - date_range : list or dict, as follows + date_range : Date range of interest, provided as start and end dates, inclusive. Accepted input date formats are: * YYYY-MM-DD string @@ -387,13 +404,13 @@ def __init__(self, date_range, start_time=None, end_time=None): Date inputs are accepted as a list or dictionary with `start_date` and `end_date` keys. Currently, a list of specific dates (rather than a range) is not accepted. TODO: allow searches with a list of dates, rather than a range. - start_time : str, datetime.time, default None + start_time : Start time in UTC/Zulu (24 hour clock). Input types are an HH:mm:ss string or datetime.time object where HH = hours, mm = minutes, ss = seconds. If None is given (and a datetime.datetime object is not supplied for `date_range`), a default of 00:00:00 is applied. - end_time : str, datetime.time, default None + end_time : End time in UTC/Zulu (24 hour clock). Input types are an HH:mm:ss string or datetime.time object where HH = hours, mm = minutes, ss = seconds. @@ -444,14 +461,14 @@ def __init__(self, date_range, start_time=None, end_time=None): "Your date range list is the wrong length. It should be of length 2, with start and end dates only." ) - def __str__(self): + def __str__(self) -> str: return "Start date and time: {0}\nEnd date and time: {1}".format( self._start.strftime("%Y-%m-%d %H:%M:%S"), self._end.strftime("%Y-%m-%d %H:%M:%S"), ) @property - def start(self): + def start(self) -> dt.datetime: """ Return the start date and time of the Temporal object as a datetime.datetime object. @@ -465,7 +482,7 @@ def start(self): return self._start @property - def end(self): + def end(self) -> dt.datetime: """ Return the end date and time of the Temporal object as a datetime.datetime object. diff --git a/icepyx/core/types.py b/icepyx/core/types.py new file mode 100644 index 000000000..e85f8696f --- /dev/null +++ b/icepyx/core/types.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import Literal, TypedDict, Union + +from typing_extensions import NotRequired + +ICESat2ProductShortName = Literal[ + "ATL01", + "ATL02", + "ATL03", + "ATL04", + "ATL06", + "ATL07", + "ATL07QL", + "ATL08", + "ATL09", + "ATL09QL", + "ATL10", + "ATL11", + "ATL12", + "ATL13", + "ATL14", + "ATL15", + "ATL16", + "ATL17", + "ATL19", + "ATL20", + "ATL21", + "ATL23", +] + +CMRParamsBase = TypedDict( + "CMRParamsBase", + { + "temporal": NotRequired[str], + "options[readable_granule_name][pattern]": NotRequired[str], + "options[spatial][or]": NotRequired[str], + "readable_granule_name[]": NotRequired[str], + }, +) + + +class CMRParamsWithBbox(CMRParamsBase): + bounding_box: str + + +class CMRParamsWithPolygon(CMRParamsBase): + polygon: str + + +CMRParams = Union[CMRParamsWithBbox, CMRParamsWithPolygon] + + +class EGIRequiredParamsBase(TypedDict): + """Common parameters for searching, ordering, or downloading from EGI. + + See: https://wiki.earthdata.nasa.gov/display/SDPSDOCS/EGI+Programmatic+Access+Documentation + + EGI shares parameters with CMR, so this data is used in conjunction with CMRParams + to build EGI requests. + + TODO: Validate more strongly (with Pydantic and its annotated types? + https://docs.pydantic.dev/latest/concepts/types/#composing-types-via-annotated): + + * version is 3 digits + * 0 < page_size <= 2000 + """ + + short_name: ICESat2ProductShortName # alias: "product" + version: str + page_size: int # default 2000 + page_num: int # default 0 + + +class EGIRequiredParamsSearch(EGIRequiredParamsBase): + """Parameters for interacting with EGI.""" + + +class EGIRequiredParamsDownload(EGIRequiredParamsBase): + """Parameters for ordering from EGI. + + TODO: Validate more strongly (with Pydantic?): page_num >=0. + """ + + request_mode: Literal["sync", "async", "stream"] # default "async" + include_meta: Literal["Y", "N"] # default "Y" + client_string: Literal["icepyx"] # default "icepyx" + # token, email + + +class EGIParamsSubsetBase(TypedDict): + """Parameters for subsetting with EGI.""" + + time: NotRequired[str] + format: NotRequired[str] + projection: NotRequired[str] + projection_parameters: NotRequired[str] + Coverage: NotRequired[str] + + +class EGIParamsSubsetBbox(EGIParamsSubsetBase): + bbox: NotRequired[str] + + +class EGIParamsSubsetBoundingShape(EGIParamsSubsetBase): + Boundingshape: NotRequired[str] + + +EGIParamsSubset = Union[EGIParamsSubsetBbox, EGIParamsSubsetBoundingShape] + +EGIRequiredParams = Union[EGIRequiredParamsSearch, EGIRequiredParamsDownload] diff --git a/icepyx/core/visualization.py b/icepyx/core/visualization.py index 0ddb9fd40..0f983b5e1 100644 --- a/icepyx/core/visualization.py +++ b/icepyx/core/visualization.py @@ -466,7 +466,7 @@ def parallel_request_OA(self) -> da.array: OA_data_da = da.concatenate(requested_OA_data, axis=0) return OA_data_da - def viz_elevation(self) -> (hv.DynamicMap, hv.Layout): + def viz_elevation(self) -> tuple[hv.DynamicMap, hv.Layout]: """ Visualize elevation requested from OpenAltimetry API using datashader based on cycles https://holoviz.org/tutorial/Large_Data.html diff --git a/icepyx/tests/__init__.py b/icepyx/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/icepyx/tests/ATL06v06_options.json b/icepyx/tests/integration/ATL06v06_options.json similarity index 100% rename from icepyx/tests/ATL06v06_options.json rename to icepyx/tests/integration/ATL06v06_options.json diff --git a/icepyx/tests/test_auth.py b/icepyx/tests/integration/test_auth.py similarity index 100% rename from icepyx/tests/test_auth.py rename to icepyx/tests/integration/test_auth.py diff --git a/icepyx/tests/test_behind_NSIDC_API_login.py b/icepyx/tests/integration/test_behind_NSIDC_API_login.py similarity index 86% rename from icepyx/tests/test_behind_NSIDC_API_login.py rename to icepyx/tests/integration/test_behind_NSIDC_API_login.py index f9fb49077..37924cfc4 100644 --- a/icepyx/tests/test_behind_NSIDC_API_login.py +++ b/icepyx/tests/integration/test_behind_NSIDC_API_login.py @@ -11,6 +11,15 @@ import icepyx as ipx import icepyx.core.is2ref as is2ref +# Skip the whole module. See: +# https://docs.pytest.org/en/stable/reference/reference.html#globalvar-pytestmark +pytestmark = pytest.mark.xfail( + reason=( + "The back-end API on which these tests depend, ECS/EGI/ESI, is scheduled for" + " shutdown in late 2024. At that point, these tests will begin failing." + ) +) + # Misc notes and needed tests # test avail data and subsetting success for each input type # (kml, shp, list of coords, bbox) @@ -40,7 +49,7 @@ def session(reg): def test_get_custom_options_output(session): obs = is2ref._get_custom_options(session, "ATL06", "006") - with open("./icepyx/tests/ATL06v06_options.json") as exp_json: + with open("./icepyx/tests/integration/ATL06v06_options.json") as exp_json: exp = json.load(exp_json) assert all(keys in obs for keys in exp) assert all(obs[key] == exp[key] for key in exp) diff --git a/icepyx/tests/test_APIformatting.py b/icepyx/tests/unit/test_APIformatting.py similarity index 100% rename from icepyx/tests/test_APIformatting.py rename to icepyx/tests/unit/test_APIformatting.py diff --git a/icepyx/tests/test_Earthdata.py b/icepyx/tests/unit/test_Earthdata.py similarity index 100% rename from icepyx/tests/test_Earthdata.py rename to icepyx/tests/unit/test_Earthdata.py diff --git a/icepyx/tests/test_granules.py b/icepyx/tests/unit/test_granules.py similarity index 100% rename from icepyx/tests/test_granules.py rename to icepyx/tests/unit/test_granules.py diff --git a/icepyx/tests/test_is2class_query.py b/icepyx/tests/unit/test_is2class_query.py similarity index 100% rename from icepyx/tests/test_is2class_query.py rename to icepyx/tests/unit/test_is2class_query.py diff --git a/icepyx/tests/test_is2ref.py b/icepyx/tests/unit/test_is2ref.py similarity index 100% rename from icepyx/tests/test_is2ref.py rename to icepyx/tests/unit/test_is2ref.py diff --git a/icepyx/tests/test_query.py b/icepyx/tests/unit/test_query.py similarity index 100% rename from icepyx/tests/test_query.py rename to icepyx/tests/unit/test_query.py diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/unit/test_quest.py similarity index 100% rename from icepyx/tests/test_quest.py rename to icepyx/tests/unit/test_quest.py diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/unit/test_quest_argo.py similarity index 100% rename from icepyx/tests/test_quest_argo.py rename to icepyx/tests/unit/test_quest_argo.py diff --git a/icepyx/tests/test_read.py b/icepyx/tests/unit/test_read.py similarity index 91% rename from icepyx/tests/test_read.py rename to icepyx/tests/unit/test_read.py index 90efe13f9..994a1aab7 100644 --- a/icepyx/tests/test_read.py +++ b/icepyx/tests/unit/test_read.py @@ -29,12 +29,12 @@ def test_parse_source_no_files(): ( # check list input [ "./icepyx/core/is2ref.py", - "./icepyx/tests/test_is2class_query.py", + "./icepyx/tests/unit/test_is2class_query.py", ], sorted( [ "./icepyx/core/is2ref.py", - "./icepyx/tests/test_is2class_query.py", + "./icepyx/tests/unit/test_is2class_query.py", ] ), ), @@ -49,8 +49,8 @@ def test_parse_source_no_files(): sorted( [ "./icepyx/core/is2ref.py", - "./icepyx/tests/test_is2class_query.py", - "./icepyx/tests/test_is2ref.py", + "./icepyx/tests/unit/test_is2class_query.py", + "./icepyx/tests/unit/test_is2ref.py", ] ), ), diff --git a/icepyx/tests/test_spatial.py b/icepyx/tests/unit/test_spatial.py similarity index 99% rename from icepyx/tests/test_spatial.py rename to icepyx/tests/unit/test_spatial.py index 2243c0674..2012699bf 100644 --- a/icepyx/tests/test_spatial.py +++ b/icepyx/tests/unit/test_spatial.py @@ -378,7 +378,7 @@ def test_bad_poly_inputfile_name_throws_error(): def test_bad_poly_inputfile_type_throws_error(): with pytest.raises(TypeError): - spat.Spatial(str(Path("./icepyx/tests/test_read.py").resolve())) + spat.Spatial(str(Path("./icepyx/tests/unit/test_read.py").resolve())) ########## geodataframe ########## diff --git a/icepyx/tests/test_temporal.py b/icepyx/tests/unit/test_temporal.py similarity index 100% rename from icepyx/tests/test_temporal.py rename to icepyx/tests/unit/test_temporal.py diff --git a/icepyx/tests/test_validate_inputs.py b/icepyx/tests/unit/test_validate_inputs.py similarity index 100% rename from icepyx/tests/test_validate_inputs.py rename to icepyx/tests/unit/test_validate_inputs.py diff --git a/icepyx/tests/test_variables.py b/icepyx/tests/unit/test_variables.py similarity index 100% rename from icepyx/tests/test_variables.py rename to icepyx/tests/unit/test_variables.py diff --git a/icepyx/tests/test_visualization.py b/icepyx/tests/unit/test_visualization.py similarity index 100% rename from icepyx/tests/test_visualization.py rename to icepyx/tests/unit/test_visualization.py diff --git a/pyproject.toml b/pyproject.toml index f44ea450a..fb4907d35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,3 +124,26 @@ ignore = [ [tool.ruff.lint.isort] force-sort-within-sections = true + + +[tool.pyright] +pythonVersion = "3.9" +# DevGoal: "strict" +typeCheckingMode = "standard" +include = [ + "icepyx", +] +exclude = [ + "**/__pycache__", + "icepyx/tests", +] +# DevGoal: Remove all ignores +ignore = [ + "icepyx/quest/*", + "icepyx/core/auth.py", + "icepyx/core/is2ref.py", + "icepyx/core/read.py", + "icepyx/core/spatial.py", + "icepyx/core/variables.py", + "icepyx/core/visualization.py", +] diff --git a/requirements-dev.txt b/requirements-dev.txt index 66106dab8..238785444 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,10 @@ +pandas-stubs pre-commit pypistats +pyright pytest>=4.6 pytest-cov responses +types-docutils +types-requests +types-tqdm diff --git a/requirements-docs.txt b/requirements-docs.txt index 51dc9ff5a..554b4d2b8 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -6,6 +6,7 @@ numpydoc pybtex pygithub sphinx>=4.3 -sphinx-panels +sphinx-autodoc-typehints>=2.0 +sphinx-design sphinx_rtd_theme>=1.0 sphinxcontrib-bibtex