Repository: biopython/biopython

Issue:
KEGG REST keeps reporting HTTP Error 403: Forbidden
### Setup

I am reporting a problem with Biopython version 1.79, Python v. 3.10, and operating
system Linux Mint 21.2 as follows:



```

from Bio.KEGG import REST
from Bio.KEGG import Compound


def cpd_decoder(cid): #gets the compound name and formula from KEGG
	if "C" in cid:
		cid="cpd:"+cid
		kegg_entry=REST.kegg_get(cid)
		for record in Compound.parse(kegg_entry):
			cid_name=record.name[0]
			cid_formula=record.formula 
			return cid_name,cid_formula

cid="C00123"
if cpd_decoder(cid) !=None:
	compound,formula=cpd_decoder(cid)

```



### Expected behaviour

Returns the name and formula of the compound of the queried CID.

### Actual behaviour
```Traceback (most recent call last):
  File "/media/tessa/Storage/Alien_Earths/Network_expansion/network expansion test 2.py", line 53, in <module>
    if cpd_decoder(i) !=None:
  File "/media/tessa/Storage/Alien_Earths/Network_expansion/network expansion test 2.py", line 27, in cpd_decoder
    kegg_entry=REST.kegg_get(cid)
  File "/home/tessa/.local/lib/python3.10/site-packages/Bio/KEGG/REST.py", line 208, in kegg_get
    resp = _q("get", dbentries)
  File "/home/tessa/.local/lib/python3.10/site-packages/Bio/KEGG/REST.py", line 44, in _q
    resp = urlopen(URL % (args))
  File "/usr/lib/python3.10/urllib/request.py", line 216, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib/python3.10/urllib/request.py", line 525, in open
    response = meth(req, response)
  File "/usr/lib/python3.10/urllib/request.py", line 634, in http_response
    response = self.parent.error(
  File "/usr/lib/python3.10/urllib/request.py", line 563, in error
    return self._call_chain(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 496, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden 
```


### Steps to reproduce

Attempt to use REST.kegg_get() to query KEGG compound databse. 


Discussion:
Can you shorten your example to a single ID you know fails?
@peterjc Done! The example should be more straight-forward now.
Changing the last three lines to just ``print(cpd_decoder("C000123"))`` is even simpler, but yes - it fails for me too.

Curiously I see Error 404 (Not found), rather than Error 403 (Forbidden).

Catching the exception and looking at the ``.url`` attribute it is trying https://rest.kegg.jp/get/cpd:C000123 which does indeed fail.

```
$ wget "https://rest.kegg.jp/get/cpd:C000123"
--2023-10-18 14:50:59--  https://rest.kegg.jp/get/cpd:C000123
Resolving rest.kegg.jp (rest.kegg.jp)... 133.103.200.27
Connecting to rest.kegg.jp (rest.kegg.jp)|133.103.200.27|:443... connected.
HTTP request sent, awaiting response... 404 Not Found
2023-10-18 14:51:01 ERROR 404: Not Found.
```

Can you find this via https://www.kegg.jp/ ? For me searching C000123 under compounds gives nothing right now. I suspect there is a problem at KEGG itself...
Wait, my bad, the CID should be C00123. 

I will also note that this problem seems to crop up when I repeatedly query the database for a long list of CIDs (100 or more)--I'm wondering if KEGG's server decides I'm a bot and blocks access.
Ah, with the fixed name it works for me:

```
>>> print(cpd_decoder("C00123"))
('L-Leucine', 'C6H13NO2')
```

I think you're probably right about having been blocked (hopefully only as a temporary measure).

It looks like we don't have a simple pause built in between calls like we do on Entrez and TogoWS. We should add something like this to the KEGG REST code:

https://github.com/biopython/biopython/blob/biopython-181/Bio/TogoWS/__init__.py#L346

Can you find any guidelines from KEGG? Would you like to make a pull request doing this?

Resolving patch:
diff --git a/Bio/KEGG/REST.py b/Bio/KEGG/REST.py
--- a/Bio/KEGG/REST.py
+++ b/Bio/KEGG/REST.py
@@ -31,9 +31,21 @@
 
 import io
 from urllib.request import urlopen
+import time
+from Bio._utils import function_with_previous
 
 
+@function_with_previous
 def _q(op, arg1, arg2=None, arg3=None):
+    delay = 0.333333333  # one third of a second
+    current = time.time()
+    wait = _q.previous + delay - current
+    if wait > 0:
+        time.sleep(wait)
+        _q.previous = current + wait
+    else:
+        _q.previous = current
+
     URL = "https://rest.kegg.jp/%s"
     if arg2 and arg3:
         args = f"{op}/{arg1}/{arg2}/{arg3}"
@@ -51,6 +63,9 @@ def _q(op, arg1, arg2=None, arg3=None):
     return handle
 
 
+_q.previous = 0
+
+
 # https://www.kegg.jp/kegg/rest/keggapi.html
 def kegg_info(database):
     """KEGG info - Displays the current statistics of a given database.


Test patch: