Repository: scholarly-python-package/scholarly

Issue:
search_pub_id()
I have really the same request as @nd7141.  At present with an *author* id I can use, e.g., `scholarly.search_author_id('jRwNfxwAAAAJ')` (and perhaps subsequently use fill) to learn what there is to learn about that author.  

I'm surprised that there's not an analogous method along the lines of `scholarly.search_pub_id('u5HHmVD_uO8C')`.    Ideally this would parse the information from https://scholar.google.com/citations?view_op=view_citation&hl=en&citation_for_view=u5HHmVD_uO8C.  

At present one can feed urls of the above form into `scholarly.search_pubs_custom_url()`, but the parser breaks.

The custom url solution you offer above *does* work, but requires knowledge not only of the publication id, but also the author id of at least one of the authors.

_Originally posted by @ligon in https://github.com/scholarly-python-package/scholarly/issues/244#issuecomment-897867099_

Discussion:

Patch:
diff --git a/scholarly/_scholarly.py b/scholarly/_scholarly.py
index b87eb04..e1d7571 100644
--- a/scholarly/_scholarly.py
+++ b/scholarly/_scholarly.py
@@ -16,6 +16,7 @@ _AUTHSEARCH = '/citations?hl=en&view_op=search_authors&mauthors={0}'
 _KEYWORDSEARCH = '/citations?hl=en&view_op=search_authors&mauthors=label:{0}'
 _KEYWORDSEARCHBASE = '/citations?hl=en&view_op=search_authors&mauthors={}'
 _PUBSEARCH = '/scholar?hl=en&q={0}'
+_CITEDBYSEARCH = '/scholar?hl=en&cites={0}'
 
 
 class _Scholarly:
@@ -123,29 +124,18 @@ class _Scholarly:
              'url_scholarbib': '/scholar?q=info:K8ZpoI6hZNoJ:scholar.google.com/&output=cite&scirp=0&hl=en'}
 
         """
-        url = _PUBSEARCH.format(requests.utils.quote(query))
-
-        yr_lo = '&as_ylo={0}'.format(year_low) if year_low is not None else ''
-        yr_hi = '&as_yhi={0}'.format(year_high) if year_high is not None else ''
-        citations = '&as_vis={0}'.format(1 - int(citations))
-        patents = '&as_sdt={0},33'.format(1 - int(patents))
-        sortby = ''
-        start = '&start={0}'.format(start_index) if start_index > 0 else ''
-
-        if sort_by == "date":
-            if include_last_year == "abstracts":
-                sortby = '&scisbd=1'
-            elif include_last_year == "everything":
-                sortby = '&scisbd=2'
-            else:
-                print("Invalid option for 'include_last_year', available options: 'everything', 'abstracts'")
-                return
-        elif sort_by != "relevance":
-            print("Invalid option for 'sort_by', available options: 'relevance', 'date'")
-            return
-            
-        # improve str below
-        url = url + yr_lo + yr_hi + citations + patents + sortby + start
+        url = _construct_url(_PUBSEARCH.format(requests.utils.quote(query)), patents=patents, citations=citations, year_low=year_low, year_high=year_high)
+        return self.__nav.search_publications(url)
+
+    def search_citedby(self, publication_id: int, **kwargs):
+        """Searches by Google Scholar publication id and returns a generator of Publication objects.
+
+        :param publication_id: Google Scholar publication id
+        :type publication_id: int
+
+        For the remaining parameters, see documentation of `search_pubs`.
+        """
+        url = _construct_url(_CITEDBYSEARCH.format(str(publication_id)), **kwargs)
         return self.__nav.search_publications(url)
 
     def search_single_pub(self, pub_title: str, filled: bool = False)->PublicationParser:
@@ -430,3 +420,33 @@ class _Scholarly:
 
         url = _AUTHSEARCH.format(requests.utils.quote(name))
         return self.__nav.search_organization(url, fromauthor)
+
+def _construct_url(baseurl: str, patents: bool = True,
+                    citations: bool = True, year_low: int = None,
+                    year_high: int = None, sort_by: str = "relevance",
+                    include_last_year: str = "abstracts",
+                    start_index: int = 0)-> str:
+    """Construct URL from requested parameters."""
+    url = baseurl
+
+    yr_lo = '&as_ylo={0}'.format(year_low) if year_low is not None else ''
+    yr_hi = '&as_yhi={0}'.format(year_high) if year_high is not None else ''
+    citations = '&as_vis={0}'.format(1 - int(citations))
+    patents = '&as_sdt={0},33'.format(1 - int(patents))
+    sortby = ''
+    start = '&start={0}'.format(start_index) if start_index > 0 else ''
+
+    if sort_by == "date":
+        if include_last_year == "abstracts":
+            sortby = '&scisbd=1'
+        elif include_last_year == "everything":
+            sortby = '&scisbd=2'
+        else:
+            print("Invalid option for 'include_last_year', available options: 'everything', 'abstracts'")
+            return
+    elif sort_by != "relevance":
+        print("Invalid option for 'sort_by', available options: 'relevance', 'date'")
+        return
+        
+    # improve str below
+    return url + yr_lo + yr_hi + citations + patents + sortby + start


Test patch:
diff --git a/test_module.py b/test_module.py
index a3c7dc6..8a593a7 100644
--- a/test_module.py
+++ b/test_module.py
@@ -117,6 +117,19 @@ class TestScholarly(unittest.TestCase):
         cites = [c for c in scholarly.citedby(filled)]
         self.assertEqual(len(cites), filled['num_citations'])
 
+    def test_search_pubs_citedby_id(self):
+        """
+        Test querying for citations by paper ID.
+
+        The 'Machine-learned epidemiology' paper had 11 citations as of
+        June 1, 2020.
+        """
+        # Machine-learned epidemiology: real-time detection of foodborne illness at scale
+        publication_id = 2244396665447968936
+        
+        pubs = [p for p in scholarly.search_citedby(publication_id)]
+        self.assertGreaterEqual(len(pubs), 11)
+
     def test_search_keyword(self):
         """
         When we search for the keyword "3d_shape" the author
@@ -232,7 +245,5 @@ class TestScholarly(unittest.TestCase):
         self.assertTrue(author_id_list[3] == 'TEndP-sAAAAJ')
 
 
-
-
 if __name__ == '__main__':
     unittest.main()