Skip to content

Commit a96a98c

Browse files
authored
Adds User-Agent header to requests (#6065)
1 parent 79286a1 commit a96a98c

File tree

2 files changed

+112
-17
lines changed

2 files changed

+112
-17
lines changed

cirq-ionq/cirq_ionq/ionq_client.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
import sys
1818
import time
1919
import urllib
20+
import platform
2021
from typing import Any, Callable, cast, Dict, List, Optional
2122
import json.decoder as jd
2223

2324
import requests
2425

2526
import cirq_ionq
2627
from cirq_ionq import ionq_exceptions
28+
from cirq import __version__ as cirq_version
2729

2830
# https://support.cloudflare.com/hc/en-us/articles/115003014512-4xx-Client-Error
2931
# "Cloudflare will generate and serve a 409 response for a Error 1001: DNS Resolution Error."
@@ -96,7 +98,7 @@ def __init__(
9698
assert max_retry_seconds >= 0, 'Negative retry not possible without time machine.'
9799

98100
self.url = f'{url.scheme}://{url.netloc}/{api_version}'
99-
self.headers = {'Authorization': f'apiKey {api_key}', 'Content-Type': 'application/json'}
101+
self.headers = self.api_headers(api_key)
100102
self.default_target = default_target
101103
self.max_retry_seconds = max_retry_seconds
102104
self.verbose = verbose
@@ -263,6 +265,34 @@ def list_calibrations(
263265
params['end'] = int((end - epoch).total_seconds() * 1000)
264266
return self._list('calibrations', params, 'calibrations', limit, batch_size)
265267

268+
def api_headers(self, api_key: str):
269+
"""API Headers needed to make calls to the REST API.
270+
271+
Args:
272+
api_key: The key used for authenticating against the IonQ API.
273+
274+
Returns:
275+
dict[str, str]: A dict of :class:`requests.Request` headers.
276+
"""
277+
return {
278+
'Authorization': f'apiKey {api_key}',
279+
'Content-Type': 'application/json',
280+
'User-Agent': self._user_agent(),
281+
}
282+
283+
def _user_agent(self):
284+
"""Generates the user agent string which is helpful in identifying
285+
different tools in the internet. Valid user-agent ionq_client header that
286+
indicates the request is from cirq_ionq along with the system, os,
287+
python,libraries details.
288+
289+
Returns:
290+
str: A string of generated user agent.
291+
"""
292+
cirq_version_string = f'cirq/{cirq_version}'
293+
python_version_string = f'python/{platform.python_version()}'
294+
return f'User-Agent: {cirq_version_string} ({python_version_string})'
295+
266296
def _target(self, target: Optional[str]) -> str:
267297
"""Returns the target if not None or the default target.
268298

cirq-ionq/cirq_ionq/ionq_client_test.py

+81-16
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def test_ionq_client_attributes():
8282
assert client.headers == {
8383
'Authorization': 'apiKey to_my_heart',
8484
'Content-Type': 'application/json',
85+
'User-Agent': client._user_agent(),
8586
}
8687
assert client.default_target == 'qpu'
8788
assert client.max_retry_seconds == 10
@@ -108,7 +109,11 @@ def test_ionq_client_create_job(mock_post):
108109
'shots': '200',
109110
'metadata': {'shots': '200', 'a': '0,1'},
110111
}
111-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
112+
expected_headers = {
113+
'Authorization': 'apiKey to_my_heart',
114+
'Content-Type': 'application/json',
115+
'User-Agent': client._user_agent(),
116+
}
112117
mock_post.assert_called_with(
113118
'http://example.com/v0.1/jobs', json=expected_json, headers=expected_headers
114119
)
@@ -261,7 +266,11 @@ def test_ionq_client_get_job_retry_409(mock_get):
261266
response = client.get_job(job_id='job_id')
262267
assert response == {'foo': 'bar'}
263268

264-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
269+
expected_headers = {
270+
'Authorization': 'apiKey to_my_heart',
271+
'Content-Type': 'application/json',
272+
'User-Agent': client._user_agent(),
273+
}
265274
mock_get.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
266275

267276

@@ -273,7 +282,11 @@ def test_ionq_client_get_job(mock_get):
273282
response = client.get_job(job_id='job_id')
274283
assert response == {'foo': 'bar'}
275284

276-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
285+
expected_headers = {
286+
'Authorization': 'apiKey to_my_heart',
287+
'Content-Type': 'application/json',
288+
'User-Agent': client._user_agent(),
289+
}
277290
mock_get.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
278291

279292

@@ -336,7 +349,11 @@ def test_ionq_client_list_jobs(mock_get):
336349
response = client.list_jobs()
337350
assert response == [{'id': '1'}, {'id': '2'}]
338351

339-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
352+
expected_headers = {
353+
'Authorization': 'apiKey to_my_heart',
354+
'Content-Type': 'application/json',
355+
'User-Agent': client._user_agent(),
356+
}
340357
mock_get.assert_called_with(
341358
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
342359
)
@@ -350,7 +367,11 @@ def test_ionq_client_list_jobs_status(mock_get):
350367
response = client.list_jobs(status='canceled')
351368
assert response == [{'id': '1'}, {'id': '2'}]
352369

353-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
370+
expected_headers = {
371+
'Authorization': 'apiKey to_my_heart',
372+
'Content-Type': 'application/json',
373+
'User-Agent': client._user_agent(),
374+
}
354375
mock_get.assert_called_with(
355376
'http://example.com/v0.1/jobs',
356377
headers=expected_headers,
@@ -367,7 +388,11 @@ def test_ionq_client_list_jobs_limit(mock_get):
367388
response = client.list_jobs(limit=2)
368389
assert response == [{'id': '1'}, {'id': '2'}]
369390

370-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
391+
expected_headers = {
392+
'Authorization': 'apiKey to_my_heart',
393+
'Content-Type': 'application/json',
394+
'User-Agent': client._user_agent(),
395+
}
371396
mock_get.assert_called_with(
372397
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
373398
)
@@ -385,7 +410,11 @@ def test_ionq_client_list_jobs_batches(mock_get):
385410
response = client.list_jobs(batch_size=1)
386411
assert response == [{'id': '1'}, {'id': '2'}, {'id': '3'}]
387412

388-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
413+
expected_headers = {
414+
'Authorization': 'apiKey to_my_heart',
415+
'Content-Type': 'application/json',
416+
'User-Agent': client._user_agent(),
417+
}
389418
url = 'http://example.com/v0.1/jobs'
390419
mock_get.assert_has_calls(
391420
[
@@ -410,7 +439,11 @@ def test_ionq_client_list_jobs_batches_does_not_divide_total(mock_get):
410439
response = client.list_jobs(batch_size=2)
411440
assert response == [{'id': '1'}, {'id': '2'}, {'id': '3'}]
412441

413-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
442+
expected_headers = {
443+
'Authorization': 'apiKey to_my_heart',
444+
'Content-Type': 'application/json',
445+
'User-Agent': client._user_agent(),
446+
}
414447
url = 'http://example.com/v0.1/jobs'
415448
mock_get.assert_has_calls(
416449
[
@@ -463,7 +496,11 @@ def test_ionq_client_cancel_job(mock_put):
463496
response = client.cancel_job(job_id='job_id')
464497
assert response == {'foo': 'bar'}
465498

466-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
499+
expected_headers = {
500+
'Authorization': 'apiKey to_my_heart',
501+
'Content-Type': 'application/json',
502+
'User-Agent': client._user_agent(),
503+
}
467504
mock_put.assert_called_with(
468505
'http://example.com/v0.1/jobs/job_id/status/cancel', headers=expected_headers
469506
)
@@ -528,7 +565,11 @@ def test_ionq_client_delete_job(mock_delete):
528565
response = client.delete_job(job_id='job_id')
529566
assert response == {'foo': 'bar'}
530567

531-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
568+
expected_headers = {
569+
'Authorization': 'apiKey to_my_heart',
570+
'Content-Type': 'application/json',
571+
'User-Agent': client._user_agent(),
572+
}
532573
mock_delete.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
533574

534575

@@ -591,7 +632,11 @@ def test_ionq_client_get_current_calibrations(mock_get):
591632
response = client.get_current_calibration()
592633
assert response == {'foo': 'bar'}
593634

594-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
635+
expected_headers = {
636+
'Authorization': 'apiKey to_my_heart',
637+
'Content-Type': 'application/json',
638+
'User-Agent': client._user_agent(),
639+
}
595640
mock_get.assert_called_with(
596641
'http://example.com/v0.1/calibrations/current', headers=expected_headers
597642
)
@@ -648,7 +693,11 @@ def test_ionq_client_list_calibrations(mock_get):
648693
response = client.list_calibrations()
649694
assert response == [{'id': '1'}, {'id': '2'}]
650695

651-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
696+
expected_headers = {
697+
'Authorization': 'apiKey to_my_heart',
698+
'Content-Type': 'application/json',
699+
'User-Agent': client._user_agent(),
700+
}
652701
mock_get.assert_called_with(
653702
'http://example.com/v0.1/calibrations',
654703
headers=expected_headers,
@@ -668,7 +717,11 @@ def test_ionq_client_list_calibrations_dates(mock_get):
668717
)
669718
assert response == [{'id': '1'}, {'id': '2'}]
670719

671-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
720+
expected_headers = {
721+
'Authorization': 'apiKey to_my_heart',
722+
'Content-Type': 'application/json',
723+
'User-Agent': client._user_agent(),
724+
}
672725
mock_get.assert_called_with(
673726
'http://example.com/v0.1/calibrations',
674727
headers=expected_headers,
@@ -687,7 +740,11 @@ def test_ionq_client_list_calibrations_limit(mock_get):
687740
response = client.list_calibrations(limit=2)
688741
assert response == [{'id': '1'}, {'id': '2'}]
689742

690-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
743+
expected_headers = {
744+
'Authorization': 'apiKey to_my_heart',
745+
'Content-Type': 'application/json',
746+
'User-Agent': client._user_agent(),
747+
}
691748
mock_get.assert_called_with(
692749
'http://example.com/v0.1/calibrations',
693750
headers=expected_headers,
@@ -708,7 +765,11 @@ def test_ionq_client_list_calibrations_batches(mock_get):
708765
response = client.list_calibrations(batch_size=1)
709766
assert response == [{'id': '1'}, {'id': '2'}, {'id': '3'}]
710767

711-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
768+
expected_headers = {
769+
'Authorization': 'apiKey to_my_heart',
770+
'Content-Type': 'application/json',
771+
'User-Agent': client._user_agent(),
772+
}
712773
url = 'http://example.com/v0.1/calibrations'
713774
mock_get.assert_has_calls(
714775
[
@@ -733,7 +794,11 @@ def test_ionq_client_list_calibrations_batches_does_not_divide_total(mock_get):
733794
response = client.list_calibrations(batch_size=2)
734795
assert response == [{'id': '1'}, {'id': '2'}, {'id': '3'}]
735796

736-
expected_headers = {'Authorization': 'apiKey to_my_heart', 'Content-Type': 'application/json'}
797+
expected_headers = {
798+
'Authorization': 'apiKey to_my_heart',
799+
'Content-Type': 'application/json',
800+
'User-Agent': client._user_agent(),
801+
}
737802
url = 'http://example.com/v0.1/calibrations'
738803
mock_get.assert_has_calls(
739804
[

0 commit comments

Comments
 (0)