qontract-reconcile 0.10.1rc729__py3-none-any.whl → 0.10.1rc731__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.
- {qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/RECORD +12 -11
- reconcile/glitchtip/integration.py +24 -21
- reconcile/glitchtip_project_alerts/integration.py +21 -18
- reconcile/glitchtip_project_dsn/integration.py +26 -23
- reconcile/jira_permissions_validator.py +9 -5
- reconcile/test/test_jira_permissions_validator.py +39 -1
- reconcile/utils/glitchtip/client.py +27 -98
- reconcile/utils/rest_api_base.py +114 -0
- {qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.1rc731
|
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
|
{qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/RECORD
RENAMED
@@ -46,7 +46,7 @@ reconcile/jenkins_roles.py,sha256=f8ELpZY36UjoaCpR_9LijQuIMuB6a7sVLFf_H1ct9Hc,44
|
|
46
46
|
reconcile/jenkins_webhooks.py,sha256=j8vhJMWcRhOdc9XzRSm0CPj84jsF3e4Syjm7r1BIsDE,1978
|
47
47
|
reconcile/jenkins_webhooks_cleaner.py,sha256=JsN_NVPfZJwv1JtSzZXDIHUqGiefL-DRffFnDGau9aY,1539
|
48
48
|
reconcile/jenkins_worker_fleets.py,sha256=PMNGOX0krubFjInPiFT0za0KCiWBLEcVDuXdKRd1BrE,5378
|
49
|
-
reconcile/jira_permissions_validator.py,sha256=
|
49
|
+
reconcile/jira_permissions_validator.py,sha256=8Nl7rSc8x_AXXVRYvRlq_arR7N5pz0YM4M7Pb60RoCg,11597
|
50
50
|
reconcile/jira_watcher.py,sha256=eyOQ92t8TFi6gogfNTO448h_h1CUyr24E0MPHc51R-o,3617
|
51
51
|
reconcile/ldap_users.py,sha256=uEWQ0V41tN9KCZi4ZKPamjrJ6djSpdpvDBo7yJ0e7ZI,3008
|
52
52
|
reconcile/mr_client_gateway.py,sha256=WhjMd-sIXDFCV8-rt8CEjurJ5OYB1pOD0K3o0tZRXQg,1885
|
@@ -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=
|
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=
|
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=
|
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
|
@@ -451,7 +451,7 @@ reconcile/test/test_gitlab_members.py,sha256=dP_dm-1THba9Vyzcq-EX1tdmBoX2hq8R-MY
|
|
451
451
|
reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
|
452
452
|
reconcile/test/test_integrations_manager.py,sha256=l6KwSFT0NS9VSR-b_9z_ZEGXDWH3EMitUEMC_1h8Xkk,38184
|
453
453
|
reconcile/test/test_jenkins_worker_fleets.py,sha256=o1jlT7OBBSgu0M3iI4xMdz_x6SciF7yhNBpLk5gTJfg,2361
|
454
|
-
reconcile/test/test_jira_permissions_validator.py,sha256=
|
454
|
+
reconcile/test/test_jira_permissions_validator.py,sha256=zhtAL97IkCyY9R29fDRvDCE1z9S7OVQV7gqu-7Vo5-4,16279
|
455
455
|
reconcile/test/test_jump_host.py,sha256=yczTqvT-hNAf9zBMuFjqka9fQOA31SCNG7D-9K9MRPw,3323
|
456
456
|
reconcile/test/test_ldap_users.py,sha256=8jjzVgoiRRylGad6-TvkugoFGXt3eko--zVVKjmZDn4,3812
|
457
457
|
reconcile/test/test_make.py,sha256=zTdjgq-3idFlec_0qJenk9wWw0QMLvSpJfPsptXmync,677
|
@@ -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=
|
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.
|
768
|
-
qontract_reconcile-0.10.
|
769
|
-
qontract_reconcile-0.10.
|
770
|
-
qontract_reconcile-0.10.
|
771
|
-
qontract_reconcile-0.10.
|
768
|
+
qontract_reconcile-0.10.1rc731.dist-info/METADATA,sha256=sursdK9csIPxAOvipsmyKrcSuaHrms1FagDc5yNyBRI,2382
|
769
|
+
qontract_reconcile-0.10.1rc731.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
770
|
+
qontract_reconcile-0.10.1rc731.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
|
771
|
+
qontract_reconcile-0.10.1rc731.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
772
|
+
qontract_reconcile-0.10.1rc731.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
|
-
|
191
|
+
with GlitchtipClient(
|
191
192
|
host=glitchtip_instance.console_url,
|
192
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
214
|
-
|
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
|
-
|
276
|
+
with GlitchtipClient(
|
276
277
|
host=glitchtip_instance.console_url,
|
277
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
208
|
+
with GlitchtipClient(
|
208
209
|
host=glitchtip_instance.console_url,
|
209
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
@@ -166,12 +166,16 @@ def board_is_valid(
|
|
166
166
|
)
|
167
167
|
error |= ValidationError.INVALID_PRIORITY
|
168
168
|
except JIRAError as e:
|
169
|
-
if e.status_code
|
169
|
+
if e.status_code == 401:
|
170
|
+
# sporadic 401 errors, retrying
|
171
|
+
logging.debug(f"[{board.name}] sporadic 401 error! Retry later.")
|
172
|
+
elif e.status_code == 403:
|
173
|
+
logging.error(
|
174
|
+
f"[{board.name}] AppSRE Jira Bot user does not have all necessary permissions. Try granting the user the administrator permissions. API URL: {e.url}"
|
175
|
+
)
|
176
|
+
error |= ValidationError.PERMISSION_ERROR
|
177
|
+
else:
|
170
178
|
raise
|
171
|
-
logging.error(
|
172
|
-
f"[{board.name}] AppSRE Jira Bot user does not have all necessary permissions. Try granting the user the administrator permissions. API URL: {e.url}"
|
173
|
-
)
|
174
|
-
error |= ValidationError.PERMISSION_ERROR
|
175
179
|
|
176
180
|
return error
|
177
181
|
|
@@ -430,7 +430,7 @@ def test_jira_permissions_validator_board_is_valid_exception(
|
|
430
430
|
},
|
431
431
|
)
|
432
432
|
jira_client = mocker.create_autospec(spec=JiraClient)
|
433
|
-
jira_client.can_create_issues.side_effect = JIRAError(status_code=
|
433
|
+
jira_client.can_create_issues.side_effect = JIRAError(status_code=500)
|
434
434
|
with pytest.raises(JIRAError):
|
435
435
|
board_is_valid(
|
436
436
|
jira=jira_client,
|
@@ -440,3 +440,41 @@ def test_jira_permissions_validator_board_is_valid_exception(
|
|
440
440
|
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
441
441
|
public_projects=[],
|
442
442
|
)
|
443
|
+
|
444
|
+
|
445
|
+
def test_jira_permissions_validator_board_is_valid_exception_401(
|
446
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
447
|
+
) -> None:
|
448
|
+
board = gql_class_factory(
|
449
|
+
JiraBoardV1,
|
450
|
+
{
|
451
|
+
"name": "jira-board-default",
|
452
|
+
"server": {
|
453
|
+
"serverUrl": "https://jira-server.com",
|
454
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
455
|
+
},
|
456
|
+
"issueType": "bug",
|
457
|
+
"issueResolveState": "Closed",
|
458
|
+
"issueReopenState": "Open",
|
459
|
+
"issueSecurityId": "32168",
|
460
|
+
"severityPriorityMappings": {
|
461
|
+
"name": "major-major",
|
462
|
+
"mappings": [
|
463
|
+
{"priority": "Minor"},
|
464
|
+
{"priority": "Major"},
|
465
|
+
{"priority": "Critical"},
|
466
|
+
],
|
467
|
+
},
|
468
|
+
},
|
469
|
+
)
|
470
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
471
|
+
jira_client.can_create_issues.side_effect = JIRAError(status_code=401)
|
472
|
+
# no error for 401
|
473
|
+
board_is_valid(
|
474
|
+
jira=jira_client,
|
475
|
+
board=board,
|
476
|
+
default_issue_type="task",
|
477
|
+
default_reopen_state="new",
|
478
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
479
|
+
public_projects=[],
|
480
|
+
)
|
@@ -1,11 +1,4 @@
|
|
1
|
-
import
|
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
|
-
|
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 [
|
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(
|
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(
|
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(
|
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(
|
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()
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc729.dist-info → qontract_reconcile-0.10.1rc731.dist-info}/top_level.txt
RENAMED
File without changes
|