qontract-reconcile 0.10.1rc729__py3-none-any.whl → 0.10.1rc730__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc729
3
+ Version: 0.10.1rc730
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -176,12 +176,12 @@ reconcile/cna/assets/asset.py,sha256=1v51uYSaD1NOc9cI_YxG7h0NOcR1ng-mkmD2UzQ8PXE
176
176
  reconcile/cna/assets/asset_factory.py,sha256=7T7X_J6xIsoGETqBRI45_EyIKEdQcnRPt_GAuVuLQcc,785
177
177
  reconcile/cna/assets/null.py,sha256=Fby1Fbn7oNRIGNasdyhRDvXJ0ktpxv-pUAPN0lZWSzk,1684
178
178
  reconcile/glitchtip/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
- reconcile/glitchtip/integration.py,sha256=te0xjFnovY199nRQeokxQjoOmDKgx-E98mBQykgMVCU,8192
179
+ reconcile/glitchtip/integration.py,sha256=Y7ofQg_xCt3dOln3pjeXp7rAnwohCgD2zcUAb-Hciis,8375
180
180
  reconcile/glitchtip/reconciler.py,sha256=nUvDv7qG1ly0cA16MmlL6NV71yl1mJYLT2mui7lmi0Y,12402
181
181
  reconcile/glitchtip_project_alerts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
- reconcile/glitchtip_project_alerts/integration.py,sha256=Bb4X-oEeBxLQtVJM4TZSerQKgDTknAWO_f-7LZRVGnI,11778
182
+ reconcile/glitchtip_project_alerts/integration.py,sha256=ADOAUysOF2iY3hVPa2xoCQPts-ydRjy_bISxaMFIxlY,11953
183
183
  reconcile/glitchtip_project_dsn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
184
- reconcile/glitchtip_project_dsn/integration.py,sha256=PlOsRaaftzODZE_uf1I-XSM732uhfvYzVc_BkYK3Zt8,7922
184
+ reconcile/glitchtip_project_dsn/integration.py,sha256=qt2a33FOUUlCyonSHm3nUb7pNXgLAq8sv_JQCm6Vj3U,8113
185
185
  reconcile/gql_definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
186
  reconcile/gql_definitions/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
187
187
  reconcile/gql_definitions/acs/acs_instances.py,sha256=L91WW9LbhJbBSrECqShQpFtjoBOsmNIYLRpMbx1io5o,2181
@@ -635,6 +635,7 @@ reconcile/utils/promtool.py,sha256=kT2rFZSBaRqW7SSHAuYzGZzQxM5Dzk8KW1NnEUYZU_s,2
635
635
  reconcile/utils/quay_api.py,sha256=EuOegpb-7ntEjkKLFwM2Oo4Nw7SyFtmyl3sQ9aXMtrM,8152
636
636
  reconcile/utils/raw_github_api.py,sha256=ZHC-SZuAyRe1zaMoOU7Krt1-zecDxENd9c_NzQYqK9g,2968
637
637
  reconcile/utils/repo_owners.py,sha256=j-pUjc9PuDzq7KpjNLpnhqfU8tUG4nj2WMhFp4ick7g,6629
638
+ reconcile/utils/rest_api_base.py,sha256=uM4fGDNx5exDky0Ls56dJsta2PpceAO_uHc10vQCjOI,3914
638
639
  reconcile/utils/ruamel.py,sha256=FzL4_L0FnMOUZmgThrZSMJs5MTdXwiy-E9MZWfk8bh8,397
639
640
  reconcile/utils/secret_reader.py,sha256=2DeYAAQFjUULEKlLw3UDAUoND6gbqvCh9uKPtlc-0us,10403
640
641
  reconcile/utils/semver_helper.py,sha256=-WfPOMSA2v1h7hT3PwVf-Htg7wOsoKlQC1JdmDX2Ars,1268
@@ -670,7 +671,7 @@ reconcile/utils/clusterhealth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
670
671
  reconcile/utils/clusterhealth/providerbase.py,sha256=6YmFCw2fbUqgte5wagJsmcM9_gJQabj0dTXKBO0RNJo,1171
671
672
  reconcile/utils/clusterhealth/telemeter.py,sha256=a_RDNQMdjELltdeSFAsheK25EEu9ElX48ksLhOOOsvI,1448
672
673
  reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
673
- reconcile/utils/glitchtip/client.py,sha256=uqRK4XL5V2_bQpRyM3XTX8JKCaMpSOCkcm7jmuqYvwU,10584
674
+ reconcile/utils/glitchtip/client.py,sha256=KHUNjN8r2J67RxROIpjzs5cKsqbwjzsM_I1TA7oLG0Q,7771
674
675
  reconcile/utils/glitchtip/models.py,sha256=_oqZXNkyRTsAnx6tF4WUURSBj0cc9UNS4okOQYfAfB4,6435
675
676
  reconcile/utils/internal_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
676
677
  reconcile/utils/internal_groups/client.py,sha256=abREA8RwXKybXFjCK8CAcCr-iUp2r0tAbIEJ-c-PXws,4538
@@ -764,8 +765,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
764
765
  tools/test/test_qontract_cli.py,sha256=UEwAW7PA_GIrbqzaLxpkCxbuVjEFLNvnVG-6VyoCGIc,4147
765
766
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
766
767
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
767
- qontract_reconcile-0.10.1rc729.dist-info/METADATA,sha256=hDn3IRWhjYB1-GMrKs0vs1GtKi4bzpd-ZeUy_YB1MiE,2382
768
- qontract_reconcile-0.10.1rc729.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
769
- qontract_reconcile-0.10.1rc729.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
770
- qontract_reconcile-0.10.1rc729.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
771
- qontract_reconcile-0.10.1rc729.dist-info/RECORD,,
768
+ qontract_reconcile-0.10.1rc730.dist-info/METADATA,sha256=qWTRMGV0QFNLswRaHu95JKig3P9sjJXkrcbUA4eDCNA,2382
769
+ qontract_reconcile-0.10.1rc730.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
770
+ qontract_reconcile-0.10.1rc730.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
771
+ qontract_reconcile-0.10.1rc730.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
772
+ qontract_reconcile-0.10.1rc730.dist-info/RECORD,,
@@ -39,6 +39,7 @@ from reconcile.utils.glitchtip import (
39
39
  User,
40
40
  )
41
41
  from reconcile.utils.internal_groups.client import InternalGroupsClient
42
+ from reconcile.utils.rest_api_base import BearerTokenAuth
42
43
  from reconcile.utils.secret_reader import (
43
44
  SecretReaderBase,
44
45
  create_secret_reader,
@@ -187,31 +188,33 @@ def run(
187
188
  if instance and glitchtip_instance.name != instance:
188
189
  continue
189
190
 
190
- glitchtip_client = GlitchtipClient(
191
+ with GlitchtipClient(
191
192
  host=glitchtip_instance.console_url,
192
- token=secret_reader.read_secret(glitchtip_instance.automation_token),
193
+ auth=BearerTokenAuth(
194
+ secret_reader.read_secret(glitchtip_instance.automation_token)
195
+ ),
193
196
  read_timeout=glitchtip_instance.read_timeout,
194
197
  max_retries=glitchtip_instance.max_retries,
195
- )
196
- current_state = fetch_current_state(
197
- glitchtip_client=glitchtip_client,
198
- # the automation user isn't managed by app-interface (chicken - egg problem), so just ignore it
199
- ignore_users=[
200
- secret_reader.read_secret(glitchtip_instance.automation_user_email)
201
- ],
202
- )
203
- desired_state = fetch_desired_state(
204
- glitchtip_projects=[
205
- p
206
- for p in glitchtip_projects
207
- if p.organization.instance.name == glitchtip_instance.name
208
- ],
209
- mail_domain=glitchtip_instance.mail_domain or "redhat.com",
210
- internal_groups_client=internal_groups_client,
211
- )
198
+ ) as glitchtip_client:
199
+ current_state = fetch_current_state(
200
+ glitchtip_client=glitchtip_client,
201
+ # the automation user isn't managed by app-interface (chicken - egg problem), so just ignore it
202
+ ignore_users=[
203
+ secret_reader.read_secret(glitchtip_instance.automation_user_email)
204
+ ],
205
+ )
206
+ desired_state = fetch_desired_state(
207
+ glitchtip_projects=[
208
+ p
209
+ for p in glitchtip_projects
210
+ if p.organization.instance.name == glitchtip_instance.name
211
+ ],
212
+ mail_domain=glitchtip_instance.mail_domain or "redhat.com",
213
+ internal_groups_client=internal_groups_client,
214
+ )
212
215
 
213
- reconciler = GlitchtipReconciler(glitchtip_client, dry_run)
214
- reconciler.reconcile(current_state, desired_state)
216
+ reconciler = GlitchtipReconciler(glitchtip_client, dry_run)
217
+ reconciler.reconcile(current_state, desired_state)
215
218
 
216
219
 
217
220
  def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
@@ -32,6 +32,7 @@ from reconcile.utils.glitchtip.models import (
32
32
  ProjectAlertRecipient,
33
33
  RecipientType,
34
34
  )
35
+ from reconcile.utils.rest_api_base import BearerTokenAuth
35
36
  from reconcile.utils.runtime.integration import (
36
37
  PydanticRunParams,
37
38
  QontractReconcileIntegration,
@@ -272,25 +273,27 @@ class GlitchtipProjectAlertsIntegration(
272
273
  else None
273
274
  )
274
275
 
275
- glitchtip_client = GlitchtipClient(
276
+ with GlitchtipClient(
276
277
  host=glitchtip_instance.console_url,
277
- token=self.secret_reader.read_secret(
278
- glitchtip_instance.automation_token
278
+ auth=BearerTokenAuth(
279
+ self.secret_reader.read_secret(glitchtip_instance.automation_token)
279
280
  ),
280
281
  read_timeout=glitchtip_instance.read_timeout,
281
282
  max_retries=glitchtip_instance.max_retries,
282
- )
283
- current_state = self.fetch_current_state(glitchtip_client=glitchtip_client)
284
- desired_state = self.fetch_desired_state(
285
- glitchtip_projects=glitchtip_projects_by_instance[
286
- glitchtip_instance.name
287
- ],
288
- gjb_alert_url=glitchtip_instance.glitchtip_jira_bridge_alert_url,
289
- gjb_token=glitchtip_jira_bridge_token,
290
- )
291
- self.reconcile(
292
- glitchtip_client=glitchtip_client,
293
- dry_run=dry_run,
294
- current_state=current_state,
295
- desired_state=desired_state,
296
- )
283
+ ) as glitchtip_client:
284
+ current_state = self.fetch_current_state(
285
+ glitchtip_client=glitchtip_client
286
+ )
287
+ desired_state = self.fetch_desired_state(
288
+ glitchtip_projects=glitchtip_projects_by_instance[
289
+ glitchtip_instance.name
290
+ ],
291
+ gjb_alert_url=glitchtip_instance.glitchtip_jira_bridge_alert_url,
292
+ gjb_token=glitchtip_jira_bridge_token,
293
+ )
294
+ self.reconcile(
295
+ glitchtip_client=glitchtip_client,
296
+ dry_run=dry_run,
297
+ current_state=current_state,
298
+ desired_state=desired_state,
299
+ )
@@ -42,6 +42,7 @@ from reconcile.utils.oc_map import (
42
42
  )
43
43
  from reconcile.utils.openshift_resource import OpenshiftResource as OR
44
44
  from reconcile.utils.openshift_resource import ResourceInventory
45
+ from reconcile.utils.rest_api_base import BearerTokenAuth
45
46
  from reconcile.utils.secret_reader import create_secret_reader
46
47
  from reconcile.utils.semver_helper import make_semver
47
48
 
@@ -204,32 +205,34 @@ def run(
204
205
  if instance and glitchtip_instance.name != instance:
205
206
  continue
206
207
 
207
- glitchtip_client = GlitchtipClient(
208
+ with GlitchtipClient(
208
209
  host=glitchtip_instance.console_url,
209
- token=secret_reader.read_secret(glitchtip_instance.automation_token),
210
+ auth=BearerTokenAuth(
211
+ secret_reader.read_secret(glitchtip_instance.automation_token)
212
+ ),
210
213
  read_timeout=glitchtip_instance.read_timeout,
211
214
  max_retries=glitchtip_instance.max_retries,
212
- )
213
- threaded.run(
214
- fetch_current_state,
215
- [
216
- p
217
- for p in glitchtip_projects
218
- if p.organization.instance.name == glitchtip_instance.name
219
- ],
220
- thread_pool_size,
221
- oc_map=oc_map,
222
- ri=ri,
223
- )
224
- fetch_desired_state(
225
- glitchtip_projects=[
226
- p
227
- for p in glitchtip_projects
228
- if p.organization.instance.name == glitchtip_instance.name
229
- ],
230
- ri=ri,
231
- glitchtip_client=glitchtip_client,
232
- )
215
+ ) as glitchtip_client:
216
+ threaded.run(
217
+ fetch_current_state,
218
+ [
219
+ p
220
+ for p in glitchtip_projects
221
+ if p.organization.instance.name == glitchtip_instance.name
222
+ ],
223
+ thread_pool_size,
224
+ oc_map=oc_map,
225
+ ri=ri,
226
+ )
227
+ fetch_desired_state(
228
+ glitchtip_projects=[
229
+ p
230
+ for p in glitchtip_projects
231
+ if p.organization.instance.name == glitchtip_instance.name
232
+ ],
233
+ ri=ri,
234
+ glitchtip_client=glitchtip_client,
235
+ )
233
236
 
234
237
  ob.publish_metrics(ri, QONTRACT_INTEGRATION)
235
238
  # create/update/delete all secrets
@@ -1,11 +1,4 @@
1
- import threading
2
- from typing import (
3
- Any,
4
- Optional,
5
- )
6
- from urllib.parse import urljoin
7
-
8
- import requests
1
+ from typing import Optional
9
2
 
10
3
  from reconcile.utils.glitchtip.models import (
11
4
  Organization,
@@ -15,94 +8,16 @@ from reconcile.utils.glitchtip.models import (
15
8
  Team,
16
9
  User,
17
10
  )
11
+ from reconcile.utils.rest_api_base import ApiBase
18
12
 
19
13
 
20
- def get_next_url(links: dict[str, dict[str, str]]) -> Optional[str]:
21
- """Parse glitchtip's response header 'Link' attribute and return the next page url if exists.
22
-
23
- See
24
- * https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/glitchtip/pagination.py#L34
25
- * https://requests.readthedocs.io/en/latest/api/?highlight=links#requests.Response.links
26
- """
27
- if links.get("next", {}).get("results", "false") == "true":
28
- return links["next"]["url"]
29
- return None
30
-
31
-
32
- class GlitchtipClient: # pylint: disable=too-many-public-methods
33
- def __init__(
34
- self,
35
- host: str,
36
- token: str,
37
- max_retries: int | None = None,
38
- read_timeout: float | None = None,
39
- ) -> None:
40
- self.host = host
41
- self.token = token
42
- self.max_retries = max_retries if max_retries is not None else 3
43
- self.read_timeout = read_timeout if read_timeout is not None else 30
44
- self._thread_local = threading.local()
45
-
46
- @property
47
- def _session(self) -> requests.Session:
48
- try:
49
- return self._thread_local.session
50
- except AttributeError:
51
- # todo timeout
52
- self._thread_local.session = requests.Session()
53
- self._thread_local.session.mount(
54
- "https://", requests.adapters.HTTPAdapter(max_retries=self.max_retries)
55
- )
56
- self._thread_local.session.mount(
57
- "http://", requests.adapters.HTTPAdapter(max_retries=self.max_retries)
58
- )
59
- self._thread_local.session.headers.update({
60
- "Authorization": f"Bearer {self.token}",
61
- "Content-Type": "application/json",
62
- })
63
- return self._thread_local.session
64
-
65
- def _get(self, url: str) -> dict[str, Any]:
66
- response = self._session.get(urljoin(self.host, url), timeout=self.read_timeout)
67
- return response.json()
68
-
69
- def _list(self, url: str, limit: int = 100) -> list[dict[str, Any]]:
70
- response = self._session.get(
71
- urljoin(self.host, url), params={"limit": limit}, timeout=self.read_timeout
72
- )
73
- response.raise_for_status()
74
- results = response.json()
75
- # handle pagination
76
- while next_url := get_next_url(response.links):
77
- response = self._session.get(next_url)
78
- results += response.json()
79
- return results
80
-
81
- def _post(self, url: str, data: Optional[dict[Any, Any]] = None) -> dict[str, Any]:
82
- response = self._session.post(
83
- urljoin(self.host, url), json=data, timeout=self.read_timeout
84
- )
85
- response.raise_for_status()
86
- if response.status_code == 204:
87
- return {}
88
- return response.json()
89
-
90
- def _put(self, url: str, data: Optional[dict[Any, Any]] = None) -> dict[str, Any]:
91
- response = self._session.put(
92
- urljoin(self.host, url), json=data, timeout=self.read_timeout
93
- )
94
- response.raise_for_status()
95
- if response.status_code == 204:
96
- return {}
97
- return response.json()
98
-
99
- def _delete(self, url: str) -> None:
100
- response = self._session.delete(urljoin(self.host, url), timeout=None)
101
- response.raise_for_status()
102
-
14
+ class GlitchtipClient(ApiBase):
103
15
  def organizations(self) -> list[Organization]:
104
16
  """List organizations."""
105
- return [Organization(**r) for r in self._list("/api/0/organizations/")]
17
+ return [
18
+ Organization(**r)
19
+ for r in self._list("/api/0/organizations/", params={"limit": 100})
20
+ ]
106
21
 
107
22
  def create_organization(self, name: str) -> Organization:
108
23
  """Create an organization."""
@@ -116,7 +31,10 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
116
31
  """List teams."""
117
32
  return [
118
33
  Team(**r)
119
- for r in self._list(f"/api/0/organizations/{organization_slug}/teams/")
34
+ for r in self._list(
35
+ f"/api/0/organizations/{organization_slug}/teams/",
36
+ params={"limit": 100},
37
+ )
120
38
  ]
121
39
 
122
40
  def create_team(self, organization_slug: str, slug: str) -> Team:
@@ -135,7 +53,10 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
135
53
  """List projects."""
136
54
  return [
137
55
  Project(**r)
138
- for r in self._list(f"/api/0/organizations/{organization_slug}/projects/")
56
+ for r in self._list(
57
+ f"/api/0/organizations/{organization_slug}/projects/",
58
+ params={"limit": 100},
59
+ )
139
60
  ]
140
61
 
141
62
  def create_project(
@@ -177,7 +98,10 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
177
98
 
178
99
  def project_key(self, organization_slug: str, project_slug: str) -> ProjectKey:
179
100
  """Retrieve project key (DSN)."""
180
- keys = self._list(f"/api/0/projects/{organization_slug}/{project_slug}/keys/")
101
+ keys = self._list(
102
+ f"/api/0/projects/{organization_slug}/{project_slug}/keys/",
103
+ params={"limit": 100},
104
+ )
181
105
  if not keys:
182
106
  # only happens if org_slug/project_slug does not exist
183
107
  raise ValueError(f"No keys found for project {project_slug}")
@@ -193,7 +117,8 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
193
117
  return [
194
118
  ProjectAlert(**r)
195
119
  for r in self._list(
196
- f"/api/0/projects/{organization_slug}/{project_slug}/alerts/"
120
+ f"/api/0/projects/{organization_slug}/{project_slug}/alerts/",
121
+ params={"limit": 100},
197
122
  )
198
123
  ]
199
124
 
@@ -247,7 +172,10 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
247
172
  """List organization users (aka members)."""
248
173
  return [
249
174
  User(**r)
250
- for r in self._list(f"/api/0/organizations/{organization_slug}/members/")
175
+ for r in self._list(
176
+ f"/api/0/organizations/{organization_slug}/members/",
177
+ params={"limit": 100},
178
+ )
251
179
  ]
252
180
 
253
181
  def invite_user(self, organization_slug: str, email: str, role: str) -> User:
@@ -277,7 +205,8 @@ class GlitchtipClient: # pylint: disable=too-many-public-methods
277
205
  return [
278
206
  User(**r)
279
207
  for r in self._list(
280
- f"/api/0/teams/{organization_slug}/{team_slug}/members/"
208
+ f"/api/0/teams/{organization_slug}/{team_slug}/members/",
209
+ params={"limit": 100},
281
210
  )
282
211
  ]
283
212
 
@@ -0,0 +1,114 @@
1
+ from typing import Any, Self
2
+ from urllib.parse import urljoin
3
+
4
+ import requests
5
+
6
+
7
+ def get_next_url(links: dict[str, dict[str, str]]) -> str | None:
8
+ """Parse response header 'Link' attribute and return the next page url if exists.
9
+
10
+ See
11
+ * https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/glitchtip/pagination.py#L34
12
+ * https://requests.readthedocs.io/en/latest/api/?highlight=links#requests.Response.links
13
+ """
14
+ if links.get("next", {}).get("results", "false") == "true":
15
+ return links["next"]["url"]
16
+ return None
17
+
18
+
19
+ class BearerTokenAuth(requests.auth.AuthBase):
20
+ """Use this class to add a Bearer token to the request headers."""
21
+
22
+ def __init__(self, token: str):
23
+ self.token = token
24
+
25
+ def __eq__(self, other: Any) -> bool:
26
+ return self.token == getattr(other, "token", None)
27
+
28
+ def __ne__(self, other: Any) -> bool:
29
+ return not self == other
30
+
31
+ def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
32
+ r.headers["Authorization"] = f"Bearer {self.token}"
33
+ return r
34
+
35
+
36
+ class ApiBase:
37
+ """This class provides a common standard for REST API clients."""
38
+
39
+ def __init__(
40
+ self,
41
+ host: str,
42
+ auth: requests.auth.AuthBase | None = None,
43
+ max_retries: int | None = None,
44
+ read_timeout: float | None = None,
45
+ session: requests.Session | None = None,
46
+ ) -> None:
47
+ self.host = host
48
+ self.max_retries = max_retries if max_retries is not None else 3
49
+ self.read_timeout = read_timeout if read_timeout is not None else 30
50
+ self.session = session or requests.Session()
51
+ if not session:
52
+ if auth:
53
+ self.session.auth = auth
54
+ self.session.mount(
55
+ "https://", requests.adapters.HTTPAdapter(max_retries=self.max_retries)
56
+ )
57
+ self.session.mount(
58
+ "http://", requests.adapters.HTTPAdapter(max_retries=self.max_retries)
59
+ )
60
+ self.session.headers.update({
61
+ "Content-Type": "application/json",
62
+ })
63
+
64
+ def __enter__(self) -> Self:
65
+ return self
66
+
67
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
68
+ self.cleanup()
69
+
70
+ def cleanup(self) -> None:
71
+ self.session.close()
72
+
73
+ def _get(self, url: str) -> dict[str, Any]:
74
+ response = self.session.get(urljoin(self.host, url), timeout=self.read_timeout)
75
+ return response.json()
76
+
77
+ def _list(
78
+ self, url: str, params: dict | None = None, attribute: str | None = None
79
+ ) -> list[dict[str, Any]]:
80
+ response = self.session.get(
81
+ urljoin(self.host, url), params=params, timeout=self.read_timeout
82
+ )
83
+ response.raise_for_status()
84
+ results = response.json()
85
+ if response.links:
86
+ # handle pagination
87
+ while next_url := get_next_url(response.links):
88
+ response = self.session.get(next_url)
89
+ results += response.json()
90
+ if attribute:
91
+ return results[attribute]
92
+ return results
93
+
94
+ def _post(self, url: str, data: dict | None = None) -> dict[str, Any]:
95
+ response = self.session.post(
96
+ urljoin(self.host, url), json=data, timeout=self.read_timeout
97
+ )
98
+ response.raise_for_status()
99
+ if response.status_code == 204:
100
+ return {}
101
+ return response.json()
102
+
103
+ def _put(self, url: str, data: dict | None = None) -> dict[str, Any]:
104
+ response = self.session.put(
105
+ urljoin(self.host, url), json=data, timeout=self.read_timeout
106
+ )
107
+ response.raise_for_status()
108
+ if response.status_code == 204:
109
+ return {}
110
+ return response.json()
111
+
112
+ def _delete(self, url: str) -> None:
113
+ response = self.session.delete(urljoin(self.host, url), timeout=None)
114
+ response.raise_for_status()