From 1323be497b743cee065b69d82eee3f9d55b3c50b Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Fri, 25 Sep 2026 14:43:35 -0500 Subject: [PATCH 1/9] make query parameter optional --- gen3/metadata.py | 72 +++++++++--------------------------------------- 1 file changed, 13 insertions(+), 59 deletions(-) diff --git a/gen3/metadata.py b/gen3/metadata.py index 4266ac4d..c3dbc30c 100644 --- a/gen3/metadata.py +++ b/gen3/metadata.py @@ -95,7 +95,7 @@ def __init__( if auth_provider and isinstance(auth_provider, Gen3Auth): endpoint = auth_provider.endpoint endpoint = endpoint.strip("/") - # if running locally, mds is deployed by itself without a location relative + # if running locally, MDS is deployed by itself without a location relative # to the commons if "http://localhost" in endpoint: service_location = "" @@ -183,11 +183,10 @@ def delete_index_key_path(self, path): @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def query( self, - query, + query=None, return_full_metadata=False, limit=10, offset=0, - use_agg_mds=False, **kwargs, ): """ @@ -224,7 +223,8 @@ def query( ''' Args: - query (str): mds query as defined by the metadata api + query (str, optional): MDS query as defined by the metadata api. If not + provided, all records are returned (subject to limit/offset) return_full_metadata (bool, optional): if False will just return a list of guids limit (int, optional): max num records to return offset (int, optional): offset for output @@ -236,7 +236,11 @@ def query( metadata JSON blobs as values """ - url = self.endpoint + f"/metadata?{query}" + url = ( + self.endpoint + f"/metadata?{query}" + if query + else self.endpoint + "/metadata" + ) url_with_params = append_query_params( url, data=return_full_metadata, limit=limit, offset=offset, **kwargs @@ -297,10 +301,10 @@ def batch_create(self, metadata_list, overwrite=True, **kwargs): Args: metadata_list (List[Dict{"guid": "", "data": {}}]): list of metadata objects in a specific format. Expects a dict with "guid" and "data" - fields where "data" is another JSON blob to add to the mds + fields where "data" is another JSON blob to add to the MDS overwrite (bool, optional): whether or not to overwrite existing data """ - url = self.admin_endpoint + f"/metadata" + url = self.admin_endpoint + "/metadata" if len(metadata_list) > 1 and ( "guid" not in metadata_list[0] and "data" not in metadata_list[0] @@ -573,6 +577,7 @@ def delete_alias(self, guid, alias, **kwargs): Args: guid (TYPE): Globally unique ID for the metadata blob + alias (str): alternative identifier (alias) to delete **kwargs: additional query params Returns: @@ -594,6 +599,7 @@ async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): Args: guid (TYPE): Globally unique ID for the metadata blob + alias (str): alternative identifier (alias) to delete _ssl (None, optional): whether or not to use ssl **kwargs: additional query params @@ -785,58 +791,6 @@ async def async_delete_aliases(self, guid, _ssl=None, **kwargs): return await response.text - @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) - def delete_alias(self, guid, alias, **kwargs): - """ - Delete single Alias for the given guid - - Args: - guid (TYPE): Globally unique ID for the metadata blob - alias (str): alternative identifier (alias) to delete - **kwargs: additional query params - - Returns: - requests.Response: response from the request to delete aliases - """ - url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}" - url_with_params = append_query_params(url, **kwargs) - - logging.debug(f"hitting: {url_with_params}") - response = requests.delete(url_with_params, auth=self._auth_provider) - response.raise_for_status() - - return response.text - - @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) - async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): - """ - Asyncronously delete single Aliases for the given guid - - Args: - guid (str): Globally unique ID for the metadata blob - alias (str): alternative identifier (alias) to delete - _ssl (None, optional): whether or not to use ssl - **kwargs: additional query params - - Returns: - requests.Response: response from the request to delete aliases - """ - async with aiohttp.ClientSession() as session: - url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}" - url_with_params = append_query_params(url, **kwargs) - - # aiohttp only allows basic auth with their built in auth, so we - # need to manually add JWT auth header - headers = {"Authorization": self._auth_provider._get_auth_value()} - - logging.debug(f"hitting: {url_with_params}") - async with session.delete( - url_with_params, headers=headers, ssl=_ssl - ) as response: - response.raise_for_status() - - return await response.text - def _prepare_metadata( self, metadata, indexd_doc, force_metadata_columns_even_if_empty ): From 4586dfee74646d0f58ca93c8f362d7e7eae06713 Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Wed, 30 Sep 2026 14:43:43 -0500 Subject: [PATCH 2/9] make gen3metadata gracefully handle gen3auth --- gen3/metadata.py | 78 ++++++++++++++++++++++++++-- tests/test_metadata.py | 115 ++++++++++++++++++++++++++++++++++------- 2 files changed, 168 insertions(+), 25 deletions(-) diff --git a/gen3/metadata.py b/gen3/metadata.py index c3dbc30c..c4e68ec6 100644 --- a/gen3/metadata.py +++ b/gen3/metadata.py @@ -4,6 +4,8 @@ import aiohttp import backoff from datetime import datetime +import functools +import inspect import requests import json import os @@ -55,10 +57,50 @@ } +def _requires_auth(func): + """ + Decorator for Gen3Metadata methods that hit admin endpoints. If the object was + created without an auth provider, log a helpful message and return None instead + of making a request that is guaranteed to fail. + """ + + def _log_missing_auth(): + logging.error( + f"'{func.__name__}' calls an /mds-admin endpoint and requires " + "authentication. Create a Gen3Auth object, e.g. " + "`auth = Gen3Auth(refresh_file='credentials.json')`, and supply it when " + "initializing: `Gen3Metadata(auth_provider=auth)`." + ) + + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(self, *args, **kwargs): + if self._auth_provider is None: + _log_missing_auth() + return None + return await func(self, *args, **kwargs) + + return async_wrapper + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if self._auth_provider is None: + _log_missing_auth() + return None + return func(self, *args, **kwargs) + + return wrapper + + class Gen3Metadata: """ A class for interacting with the Gen3 Metadata services. + Open access endpoints work without authentication. Methods that call admin + endpoints (create, update, delete, index management, alias changes) require an + auth_provider; without one they log a message and return directly. + Examples: This generates the Gen3Metadata class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. @@ -94,6 +136,11 @@ def __init__( endpoint = None if auth_provider and isinstance(auth_provider, Gen3Auth): endpoint = auth_provider.endpoint + if not endpoint: + raise ValueError( + "Provide either an endpoint or a Gen3Auth object (auth_provider) " + "to initialize Gen3Metadata." + ) endpoint = endpoint.strip("/") # if running locally, MDS is deployed by itself without a location relative # to the commons @@ -139,6 +186,7 @@ def get_version(self): return response.text @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def get_index_key_paths(self): """ List all the metadata key paths indexed in the database. @@ -153,6 +201,7 @@ def get_index_key_paths(self): return response.json() @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def create_index_key_path(self, path): """ Create a metadata key path indexed in the database. @@ -167,6 +216,7 @@ def create_index_key_path(self, path): return response.json() @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def delete_index_key_path(self, path): """ List all the metadata key paths indexed in the database. @@ -294,6 +344,7 @@ def get(self, guid, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def batch_create(self, metadata_list, overwrite=True, **kwargs): """ Create the list of metadata associated with the list of guids @@ -327,6 +378,7 @@ def batch_create(self, metadata_list, overwrite=True, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def create(self, guid, metadata, aliases=None, overwrite=False, **kwargs): """ Create the metadata associated with the guid @@ -363,6 +415,7 @@ def create(self, guid, metadata, aliases=None, overwrite=False, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_create( self, guid, @@ -417,6 +470,7 @@ async def async_create( return response @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def update(self, guid, metadata, aliases=None, merge=False, **kwargs): """ Update the metadata associated with the guid @@ -452,6 +506,7 @@ def update(self, guid, metadata, aliases=None, merge=False, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth async def async_update( self, guid, metadata, aliases=None, merge=False, _ssl=None, **kwargs ): @@ -500,6 +555,7 @@ async def async_update( return response @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def delete(self, guid, **kwargs): """ Delete the metadata associated with the guid @@ -560,7 +616,11 @@ async def async_get_aliases(self, guid, _ssl=None, **kwargs): # aiohttp only allows basic auth with their built in auth, so we # need to manually add JWT auth header - headers = {"Authorization": self._auth_provider._get_auth_value()} + headers = ( + {"Authorization": self._auth_provider._get_auth_value()} + if self._auth_provider + else {} + ) logging.debug(f"hitting: {url_with_params}") async with session.get( @@ -571,6 +631,7 @@ async def async_get_aliases(self, guid, _ssl=None, **kwargs): return await response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def delete_alias(self, guid, alias, **kwargs): """ Delete single Alias for the given guid @@ -593,9 +654,10 @@ def delete_alias(self, guid, alias, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): """ - Asyncronously delete single Aliases for the given guid + Asynchronously delete single Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -623,6 +685,7 @@ async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): return await response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def create_aliases(self, guid, aliases, **kwargs): """ Create Aliases for the given guid @@ -648,9 +711,10 @@ def create_aliases(self, guid, aliases, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_create_aliases(self, guid, aliases, _ssl=None, **kwargs): """ - Asyncronously create Aliases for the given guid + Asynchronously create Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -680,6 +744,7 @@ async def async_create_aliases(self, guid, aliases, _ssl=None, **kwargs): return await response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def update_aliases(self, guid, aliases, merge=False, **kwargs): """ Update Aliases for the given guid @@ -706,11 +771,12 @@ def update_aliases(self, guid, aliases, merge=False, **kwargs): return response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_update_aliases( self, guid, aliases, merge=False, _ssl=None, **kwargs ): """ - Asyncronously update Aliases for the given guid + Asynchronously update Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -742,6 +808,7 @@ async def async_update_aliases( return await response.json() @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def delete_aliases(self, guid, **kwargs): """ Delete all Aliases for the given guid @@ -763,9 +830,10 @@ def delete_aliases(self, guid, **kwargs): return response.text @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_delete_aliases(self, guid, _ssl=None, **kwargs): """ - Asyncronously delete all Aliases for the given guid + Asynchronously delete all Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 21aa55aa..81f67031 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -126,11 +126,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.get") -def test_get_index_key_paths(requests_mock): +def test_get_index_key_paths(requests_mock, gen3_auth): """ Test getting index key paths """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) expected_response = ["abc"] def _mock_request(url, **kwargs): @@ -151,11 +151,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.get") -def test_get_index_key_paths_error(requests_mock): +def test_get_index_key_paths_error(requests_mock, gen3_auth): """ Test getting key paths error """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) def _mock_request(url, **kwargs): assert url.endswith("/metadata_index") @@ -174,11 +174,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.post") -def test_create_index_key_paths(requests_mock): +def test_create_index_key_paths(requests_mock, gen3_auth): """ Test creating index key paths """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) path = "/blah" expected_response = path @@ -200,11 +200,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.post") -def test_create_index_key_paths_error(requests_mock): +def test_create_index_key_paths_error(requests_mock, gen3_auth): """ Test create index key paths error """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) path = "/blah" def _mock_request(url, **kwargs): @@ -224,11 +224,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.delete") -def test_delete_index_key_path(requests_mock): +def test_delete_index_key_path(requests_mock, gen3_auth): """ Test deleting the index key path """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) path = "/blah" expected_response = {} @@ -250,11 +250,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.delete") -def test_delete_index_key_paths_error(requests_mock): +def test_delete_index_key_paths_error(requests_mock, gen3_auth): """ Test deleting the index key path error """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) path = "/blah" expected_response = {} @@ -363,11 +363,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.post") -def test_batch_create(requests_mock): +def test_batch_create(requests_mock, gen3_auth): """ Test batch creation """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) metadata_list = [ {"guid": "3c42c819-1dfe-4c3e-8d46-c3ec7eb99bf4", "data": {"foo": "bar"}}, {"guid": "dfa1a1dc-98f4-46be-ba8f-ae9b42b0ee50", "data": {"foo": "bar"}}, @@ -403,11 +403,11 @@ def _mock_request(url, **kwargs): [None, [], ["1"], ["1a", "2b", "3c"], ["A!@SJD)(*@#Q&)AS(DF"]], ) @patch("gen3.metadata.requests.post") -def test_create(requests_mock, aliases): +def test_create(requests_mock, aliases, gen3_auth): """ Test creating for guids """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) guid = "95a41871-244c-48ae-8004-63f4ed1f0291" data = {"foo": "bar", "fizz": "buzz", "nested_details": {"key1": "value1"}} expected_response = data @@ -438,11 +438,11 @@ def _mock_request(url, **kwargs): [None, [], ["1"], ["1a", "2b", "3c"], ["A!@SJD)(*@#Q&)AS(DF"]], ) @patch("gen3.metadata.requests.put") -def test_update(requests_mock, aliases): +def test_update(requests_mock, aliases, gen3_auth): """ Test updating for guids """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) guid = "95a41871-244c-48ae-8004-63f4ed1f0291" data = {"foo": "bar", "fizz": "buzz", "nested_details": {"key1": "value1"}} expected_response = data @@ -469,11 +469,11 @@ def _mock_request(url, **kwargs): @patch("gen3.metadata.requests.delete") -def test_delete(requests_mock): +def test_delete(requests_mock, gen3_auth): """ Test deleting guids """ - metadata = Gen3Metadata("https://example.com") + metadata = Gen3Metadata("https://example.com", auth_provider=gen3_auth) guid = "95a41871-244c-48ae-8004-63f4ed1f0291" expected_response = {} @@ -628,3 +628,78 @@ def _mock_get_request(url, **kwargs): response = mds.get_aliases(guid) assert response.get("aliases") == [] + + +def test_init_requires_endpoint_or_auth(): + """ + Neither an endpoint nor an auth provider is an error + """ + with pytest.raises(ValueError): + Gen3Metadata() + + +@patch("gen3.metadata.requests.get") +def test_open_endpoints_without_auth(requests_mock): + """ + Open access endpoints work when no auth provider is supplied + """ + metadata = Gen3Metadata("https://example.com") + mock_response = MagicMock() + mock_response.json.return_value = {"guid": {}} + mock_response.text = "1.0" + requests_mock.return_value = mock_response + + assert metadata.query("a=1") == {"guid": {}} + assert metadata.get("guid") == {"guid": {}} + assert metadata.get_aliases("guid") == {"guid": {}} + assert metadata.get_version() == "1.0" + assert requests_mock.call_count == 4 + for call in requests_mock.call_args_list: + assert call.kwargs["auth"] is None + + +@patch("gen3.metadata.requests.delete") +@patch("gen3.metadata.requests.put") +@patch("gen3.metadata.requests.post") +@patch("gen3.metadata.requests.get") +@patch("gen3.metadata.logging") +def test_admin_endpoints_without_auth( + mock_logging, requests_get, requests_post, requests_put, requests_delete +): + """ + Admin methods log a helpful message and make no request without an auth provider + """ + metadata = Gen3Metadata("https://example.com") + calls = [ + (metadata.get_index_key_paths, ()), + (metadata.create_index_key_path, ("path",)), + (metadata.delete_index_key_path, ("path",)), + (metadata.batch_create, ([{"guid": "g", "data": {}}],)), + (metadata.create, ("g", {})), + (metadata.update, ("g", {})), + (metadata.delete, ("g",)), + (metadata.delete_alias, ("g", "a")), + (metadata.create_aliases, ("g", ["a"])), + (metadata.update_aliases, ("g", ["a"])), + (metadata.delete_aliases, ("g",)), + ] + async_calls = [ + (metadata.async_create, ("g", {})), + (metadata.async_update, ("g", {})), + (metadata.async_delete_alias, ("g", "a")), + (metadata.async_create_aliases, ("g", ["a"])), + (metadata.async_update_aliases, ("g", ["a"])), + (metadata.async_delete_aliases, ("g",)), + ] + loop = get_or_create_event_loop_for_thread() + for func, args in calls: + mock_logging.reset_mock() + assert func(*args) is None + assert "Gen3Auth" in mock_logging.error.call_args.args[0], func.__name__ + for func, args in async_calls: + mock_logging.reset_mock() + assert loop.run_until_complete(func(*args)) is None + assert "Gen3Auth" in mock_logging.error.call_args.args[0], func.__name__ + + for mock in (requests_get, requests_post, requests_put, requests_delete): + mock.assert_not_called() From 676446ca2f5bc2df2baa21f7376af888f48abd8a Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Wed, 30 Sep 2026 16:31:14 -0500 Subject: [PATCH 3/9] update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b0571e38..4901ff0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "gen3" homepage = "https://gen3.org/" -version = "4.28.4" +version = "4.28.5" description = "Gen3 CLI and Python SDK" authors = ["Center for Translational Data Science at the University of Chicago "] license = "Apache-2.0" From ac7d2eaa16ef4498417a63330cd1e3216140fdbe Mon Sep 17 00:00:00 2001 From: mfshao Date: Wed, 30 Sep 2026 21:32:12 +0000 Subject: [PATCH 4/9] Apply automatic documentation changes --- docs/_build/doctrees/metadata.doctree | Bin 138577 -> 139418 bytes docs/_build/html/_modules/gen3/metadata.html | 166 +++++++++++-------- docs/_build/html/metadata.html | 20 ++- docs/_build/html/searchindex.js | 2 +- 4 files changed, 107 insertions(+), 81 deletions(-) diff --git a/docs/_build/doctrees/metadata.doctree b/docs/_build/doctrees/metadata.doctree index abb2e69124d8c4678fdf4e7b78e2cf4bbc5ace78..53c986e5a7a09fcadede0c573377d70794af123f 100644 GIT binary patch literal 139418 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 code for gen3.metadata
 import aiohttp
 import backoff
 from datetime import datetime
+import functools
+import inspect
 import requests
 import json
 import os
@@ -88,12 +90,52 @@ 

Source code for gen3.metadata

 }
 
 
+def _requires_auth(func):
+    """
+    Decorator for Gen3Metadata methods that hit admin endpoints. If the object was
+    created without an auth provider, log a helpful message and return None instead
+    of making a request that is guaranteed to fail.
+    """
+
+    def _log_missing_auth():
+        logging.error(
+            f"'{func.__name__}' calls an /mds-admin endpoint and requires "
+            "authentication. Create a Gen3Auth object, e.g. "
+            "`auth = Gen3Auth(refresh_file='credentials.json')`, and supply it when "
+            "initializing: `Gen3Metadata(auth_provider=auth)`."
+        )
+
+    if inspect.iscoroutinefunction(func):
+
+        @functools.wraps(func)
+        async def async_wrapper(self, *args, **kwargs):
+            if self._auth_provider is None:
+                _log_missing_auth()
+                return None
+            return await func(self, *args, **kwargs)
+
+        return async_wrapper
+
+    @functools.wraps(func)
+    def wrapper(self, *args, **kwargs):
+        if self._auth_provider is None:
+            _log_missing_auth()
+            return None
+        return func(self, *args, **kwargs)
+
+    return wrapper
+
+
 
[docs] class Gen3Metadata: """ A class for interacting with the Gen3 Metadata services. + Open access endpoints work without authentication. Methods that call admin + endpoints (create, update, delete, index management, alias changes) require an + auth_provider; without one they log a message and return directly. + Examples: This generates the Gen3Metadata class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. @@ -129,8 +171,13 @@

Source code for gen3.metadata

             endpoint = None
         if auth_provider and isinstance(auth_provider, Gen3Auth):
             endpoint = auth_provider.endpoint
+        if not endpoint:
+            raise ValueError(
+                "Provide either an endpoint or a Gen3Auth object (auth_provider) "
+                "to initialize Gen3Metadata."
+            )
         endpoint = endpoint.strip("/")
-        # if running locally, mds is deployed by itself without a location relative
+        # if running locally, MDS is deployed by itself without a location relative
         # to the commons
         if "http://localhost" in endpoint:
             service_location = ""
@@ -182,6 +229,7 @@ 

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def get_index_key_paths(self): """ List all the metadata key paths indexed in the database. @@ -199,6 +247,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def create_index_key_path(self, path): """ Create a metadata key path indexed in the database. @@ -216,6 +265,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def delete_index_key_path(self, path): """ List all the metadata key paths indexed in the database. @@ -235,11 +285,10 @@

Source code for gen3.metadata

     @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
     def query(
         self,
-        query,
+        query=None,
         return_full_metadata=False,
         limit=10,
         offset=0,
-        use_agg_mds=False,
         **kwargs,
     ):
         """
@@ -276,7 +325,8 @@ 

Source code for gen3.metadata

             '''
 
         Args:
-            query (str): mds query as defined by the metadata api
+            query (str, optional): MDS query as defined by the metadata api. If not
+                provided, all records are returned (subject to limit/offset)
             return_full_metadata (bool, optional): if False will just return a list of guids
             limit (int, optional): max num records to return
             offset (int, optional): offset for output
@@ -288,7 +338,11 @@ 

Source code for gen3.metadata

                 metadata JSON blobs as values
         """
 
-        url = self.endpoint + f"/metadata?{query}"
+        url = (
+            self.endpoint + f"/metadata?{query}"
+            if query
+            else self.endpoint + "/metadata"
+        )
 
         url_with_params = append_query_params(
             url, data=return_full_metadata, limit=limit, offset=offset, **kwargs
@@ -351,6 +405,7 @@ 

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def batch_create(self, metadata_list, overwrite=True, **kwargs): """ Create the list of metadata associated with the list of guids @@ -358,10 +413,10 @@

Source code for gen3.metadata

         Args:
             metadata_list (List[Dict{"guid": "", "data": {}}]): list of metadata
                 objects in a specific format. Expects a dict with "guid" and "data"
-                fields where "data" is another JSON blob to add to the mds
+                fields where "data" is another JSON blob to add to the MDS
             overwrite (bool, optional): whether or not to overwrite existing data
         """
-        url = self.admin_endpoint + f"/metadata"
+        url = self.admin_endpoint + "/metadata"
 
         if len(metadata_list) > 1 and (
             "guid" not in metadata_list[0] and "data" not in metadata_list[0]
@@ -387,6 +442,7 @@ 

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def create(self, guid, metadata, aliases=None, overwrite=False, **kwargs): """ Create the metadata associated with the guid @@ -426,6 +482,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_create( self, guid, @@ -483,6 +540,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def update(self, guid, metadata, aliases=None, merge=False, **kwargs): """ Update the metadata associated with the guid @@ -521,6 +579,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth async def async_update( self, guid, metadata, aliases=None, merge=False, _ssl=None, **kwargs ): @@ -572,6 +631,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) + @_requires_auth def delete(self, guid, **kwargs): """ Delete the metadata associated with the guid @@ -638,7 +698,11 @@

Source code for gen3.metadata

 
             # aiohttp only allows basic auth with their built in auth, so we
             # need to manually add JWT auth header
-            headers = {"Authorization": self._auth_provider._get_auth_value()}
+            headers = (
+                {"Authorization": self._auth_provider._get_auth_value()}
+                if self._auth_provider
+                else {}
+            )
 
             logging.debug(f"hitting: {url_with_params}")
             async with session.get(
@@ -649,13 +713,17 @@ 

Source code for gen3.metadata

             return await response.json()
+
+[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def delete_alias(self, guid, alias, **kwargs): """ Delete single Alias for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob + alias (str): alternative identifier (alias) to delete **kwargs: additional query params Returns: @@ -668,15 +736,20 @@

Source code for gen3.metadata

         response = requests.delete(url_with_params, auth=self._auth_provider)
         response.raise_for_status()
 
-        return response.json()
+        return response.json()
+ +
+[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): """ - Asyncronously delete single Aliases for the given guid + Asynchronously delete single Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob + alias (str): alternative identifier (alias) to delete _ssl (None, optional): whether or not to use ssl **kwargs: additional query params @@ -697,11 +770,13 @@

Source code for gen3.metadata

             ) as response:
                 response.raise_for_status()
 
-            return await response.json()
+            return await response.json()
+
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def create_aliases(self, guid, aliases, **kwargs): """ Create Aliases for the given guid @@ -730,9 +805,10 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_create_aliases(self, guid, aliases, _ssl=None, **kwargs): """ - Asyncronously create Aliases for the given guid + Asynchronously create Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -765,6 +841,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def update_aliases(self, guid, aliases, merge=False, **kwargs): """ Update Aliases for the given guid @@ -794,11 +871,12 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_update_aliases( self, guid, aliases, merge=False, _ssl=None, **kwargs ): """ - Asyncronously update Aliases for the given guid + Asynchronously update Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -833,6 +911,7 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth def delete_aliases(self, guid, **kwargs): """ Delete all Aliases for the given guid @@ -857,9 +936,10 @@

Source code for gen3.metadata

 
[docs] @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) + @_requires_auth async def async_delete_aliases(self, guid, _ssl=None, **kwargs): """ - Asyncronously delete all Aliases for the given guid + Asynchronously delete all Aliases for the given guid Args: guid (TYPE): Globally unique ID for the metadata blob @@ -886,64 +966,6 @@

Source code for gen3.metadata

                 return await response.text
-
-[docs] - @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) - def delete_alias(self, guid, alias, **kwargs): - """ - Delete single Alias for the given guid - - Args: - guid (TYPE): Globally unique ID for the metadata blob - alias (str): alternative identifier (alias) to delete - **kwargs: additional query params - - Returns: - requests.Response: response from the request to delete aliases - """ - url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}" - url_with_params = append_query_params(url, **kwargs) - - logging.debug(f"hitting: {url_with_params}") - response = requests.delete(url_with_params, auth=self._auth_provider) - response.raise_for_status() - - return response.text
- - -
-[docs] - @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED) - async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs): - """ - Asyncronously delete single Aliases for the given guid - - Args: - guid (str): Globally unique ID for the metadata blob - alias (str): alternative identifier (alias) to delete - _ssl (None, optional): whether or not to use ssl - **kwargs: additional query params - - Returns: - requests.Response: response from the request to delete aliases - """ - async with aiohttp.ClientSession() as session: - url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}" - url_with_params = append_query_params(url, **kwargs) - - # aiohttp only allows basic auth with their built in auth, so we - # need to manually add JWT auth header - headers = {"Authorization": self._auth_provider._get_auth_value()} - - logging.debug(f"hitting: {url_with_params}") - async with session.delete( - url_with_params, headers=headers, ssl=_ssl - ) as response: - response.raise_for_status() - - return await response.text
- - def _prepare_metadata( self, metadata, indexd_doc, force_metadata_columns_even_if_empty ): diff --git a/docs/_build/html/metadata.html b/docs/_build/html/metadata.html index 868e4e43..cd2655cf 100644 --- a/docs/_build/html/metadata.html +++ b/docs/_build/html/metadata.html @@ -40,6 +40,9 @@

Gen3 Metadata Classclass gen3.metadata.Gen3Metadata(endpoint=None, auth_provider=None, service_location='mds', admin_endpoint_suffix='-admin')[source]

Bases: object

A class for interacting with the Gen3 Metadata services.

+

Open access endpoints work without authentication. Methods that call admin +endpoints (create, update, delete, index management, alias changes) require an +auth_provider; without one they log a message and return directly.

Examples

This generates the Gen3Metadata class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

@@ -89,7 +92,7 @@

Gen3 Metadata Class
async async_create_aliases(guid, aliases, _ssl=None, **kwargs)[source]
-

Asyncronously create Aliases for the given guid

+

Asynchronously create Aliases for the given guid

Parameters:

@@ -770,7 +770,7 @@

Source code for gen3.metadata

             ) as response:
                 response.raise_for_status()
 
-            return await response.json()
+ return await response.text()
From c4196d81dbcf4d814a56540c47577a67efe45c5a Mon Sep 17 00:00:00 2001 From: Mingfei Shao Date: Wed, 7 Oct 2026 12:54:45 -0500 Subject: [PATCH 9/9] fix test --- tests/test_discovery.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 8b0ee378..a45e5a83 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -260,14 +260,12 @@ def test_discovery_tsv_export_with_large_limit_reads_multiple_pages( return_full_metadata=True, limit=2000, offset=0, - use_agg_mds=False, ), call( "_guid_type=discovery_metadata", return_full_metadata=True, limit=2000, offset=2000, - use_agg_mds=False, ), ]