canonicalwebteam.store-api 5.0.0__py3-none-any.whl → 6.0.0__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.
@@ -0,0 +1,647 @@
1
+ from os import getenv
2
+ from requests import Session
3
+ from pymacaroons import Macaroon
4
+ from typing import Optional, List
5
+
6
+ from canonicalwebteam.store_api.base import Base
7
+ from canonicalwebteam.exceptions import (
8
+ PublisherMacaroonRefreshRequired,
9
+ )
10
+
11
+
12
+ DASHBOARD_API_URL = getenv(
13
+ "DASHBOARD_API_URL", "https://dashboard.snapcraft.io/"
14
+ )
15
+
16
+
17
+ class Dashboard(Base):
18
+ def __init__(self, session=Session()):
19
+ super().__init__(session)
20
+
21
+ self.config = {
22
+ 1: {"base_url": f"{DASHBOARD_API_URL}dev/api/"},
23
+ 2: {"base_url": f"{DASHBOARD_API_URL}api/v2/"},
24
+ }
25
+
26
+ def get_endpoint_url(
27
+ self, endpoint: str, api_version: int = 1, is_store: bool = False
28
+ ) -> str:
29
+ if is_store:
30
+ base_url = self.config[api_version]["base_url"]
31
+ return f"{base_url}stores/{endpoint}"
32
+ base_url = self.config[api_version]["base_url"]
33
+ return f"{base_url}{endpoint}"
34
+
35
+ def _get_authorization_header(self, session: dict) -> dict:
36
+ """
37
+ Bind root and discharge macaroons and return the authorization header.
38
+ """
39
+ if "macaroon_root" in session:
40
+ root = session["macaroon_root"]
41
+ discharge = session["macaroon_discharge"]
42
+
43
+ bound = (
44
+ Macaroon.deserialize(root)
45
+ .prepare_for_request(Macaroon.deserialize(discharge))
46
+ .serialize()
47
+ )
48
+
49
+ return {
50
+ "Authorization": f"macaroon root={root}, discharge={bound}"
51
+ }
52
+ # With Candid the header is Macaroons
53
+ elif "macaroons" in session:
54
+ return {"Macaroons": session["macaroons"]}
55
+ return {"Macaroons": ""}
56
+
57
+ def get_macaroon(self, permissions: List[str]) -> str:
58
+ """
59
+ Return a bakery v2 macaroon from the publisher API to be discharged
60
+ Documemntation:
61
+ https://dashboard.snapcraft.io/docs/reference/v1/macaroon.html
62
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/acl
63
+ """
64
+ response: dict = self.session.post(
65
+ url=self.get_endpoint_url("tokens", 2),
66
+ json={"permissions": permissions},
67
+ )
68
+
69
+ return self.process_response(response)["macaroon"]
70
+
71
+ def get_account(self, session: dict) -> dict:
72
+ """
73
+ Documentation:
74
+ https://dashboard.snapcraft.io/docs/reference/v1/account.html#get--dev-api-account
75
+ Endpoint: [GET] https://dashboard.snapcraft.io/dev/api/account
76
+ """
77
+ headers = self._get_authorization_header(session)
78
+ response = self.session.get(
79
+ url=self.get_endpoint_url("account"), headers=headers
80
+ )
81
+ return self.process_response(response)
82
+
83
+ def get_account_snaps(self, session: dict) -> dict:
84
+ """
85
+ Returns the snaps associated with a user account
86
+ Documentation:
87
+ https://dashboard.snapcraft.io/docs/reference/v1/account.html#get--dev-api-account
88
+ Endpoint: [GET] https://dashboard.snapcraft.io/dev/api/account
89
+ """
90
+ return self.get_account(session).get("snaps", {}).get("16", {})
91
+
92
+ def get_agreement(self, session: dict) -> dict:
93
+ """
94
+ Documentation:
95
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#release-a-snap-build-to-a-channel
96
+ Endpoint: [GET] https://dashboard.snapcraft.io/dev/api/agreement
97
+ """
98
+ headers = self._get_authorization_header(session)
99
+ agreement_response = self.session.get(
100
+ url=self.get_endpoint_url("agreement/"), headers=headers
101
+ )
102
+
103
+ if self._is_macaroon_expired(agreement_response.headers):
104
+ raise PublisherMacaroonRefreshRequired
105
+
106
+ return agreement_response.json()
107
+
108
+ def post_agreement(self, session: dict, agreed: bool) -> dict:
109
+ """
110
+ Documentation:
111
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#release-a-snap-build-to-a-channel
112
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/agreement
113
+ """
114
+ headers = self._get_authorization_header(session)
115
+
116
+ json = {"latest_tos_accepted": agreed}
117
+ agreement_response = self.session.post(
118
+ url=self.get_endpoint_url("agreement/"), headers=headers, json=json
119
+ )
120
+
121
+ return self.process_response(agreement_response)
122
+
123
+ def post_username(self, session: dict, username: str) -> dict:
124
+ """
125
+ Documentation:
126
+ https://dashboard.snapcraft.io/docs/reference/v1/account.html#get--dev-api-account
127
+ Endpoint: [PATCH] https://dashboard.snapcraft.io/dev/api/account
128
+ """
129
+ headers = self._get_authorization_header(session)
130
+ json = {"short_namespace": username}
131
+ username_response = self.session.patch(
132
+ url=self.get_endpoint_url("account"), headers=headers, json=json
133
+ )
134
+
135
+ if username_response.status_code == 204:
136
+ return {}
137
+ else:
138
+ return self.process_response(username_response)
139
+
140
+ def post_register_name(
141
+ self,
142
+ session: dict,
143
+ snap_name: str,
144
+ registrant_comment: str = "",
145
+ is_private: str = "",
146
+ store: str = "",
147
+ ) -> dict:
148
+ """
149
+ Documentation:
150
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#register-a-snap-name
151
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/register-name/
152
+ """
153
+ json = {"snap_name": snap_name}
154
+
155
+ if registrant_comment:
156
+ json["registrant_comment"] = registrant_comment
157
+
158
+ if is_private:
159
+ json["is_private"] = is_private
160
+
161
+ if store:
162
+ json["store"] = store
163
+
164
+ response = self.session.post(
165
+ url=self.get_endpoint_url("register-name/"),
166
+ headers=self._get_authorization_header(session),
167
+ json=json,
168
+ )
169
+
170
+ return self.process_response(response)
171
+
172
+ def post_register_name_dispute(
173
+ self, session: dict, snap_name: str, claim_comment: str
174
+ ) -> dict:
175
+ """
176
+ Documentation:
177
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#register-a-snap-name-dispute
178
+ Endpoint: [POST]
179
+ https://dashboard.snapcraft.io/dev/api/register-name-dispute
180
+ """
181
+ json = {"snap_name": snap_name, "comment": claim_comment}
182
+
183
+ response = self.session.post(
184
+ url=self.get_endpoint_url("register-name-dispute/"),
185
+ headers=self._get_authorization_header(session),
186
+ json=json,
187
+ )
188
+
189
+ return self.process_response(response)
190
+
191
+ def get_snap_info(self, session: dict, snap_name: str) -> dict:
192
+ """
193
+ Documentation:
194
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#obtaining-information-about-a-snap
195
+ Endpoint: [GET]
196
+ https://dashboard.snapcraft.io/dev/api/snaps/info/{snap_name}
197
+ """
198
+ response = self.session.get(
199
+ url=self.get_endpoint_url(f"snaps/info/{snap_name}"),
200
+ headers=self._get_authorization_header(session),
201
+ )
202
+
203
+ return self.process_response(response)
204
+
205
+ def get_package_upload_macaroon(
206
+ self, session: dict, snap_name: str, channels: List[str]
207
+ ) -> dict:
208
+ """
209
+ Documentation:
210
+ https://dashboard.snapcraft.io/docs/reference/v1/macaroon.html#request-a-macaroon
211
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/acl/
212
+ """
213
+ json = {
214
+ "packages": [{"name": snap_name, "series": "16"}],
215
+ "permissions": ["package_upload"],
216
+ "channels": channels,
217
+ }
218
+
219
+ response = self.session.post(
220
+ url=self.get_endpoint_url("acl/"),
221
+ headers=self._get_authorization_header(session),
222
+ json=json,
223
+ )
224
+
225
+ return self.process_response(response)
226
+
227
+ def get_snap_id(self, session: dict, snap_name: str) -> str:
228
+ """
229
+ Documentation:
230
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#obtaining-information-about-a-snap
231
+ Endpoint: https://dashboard.snapcraft.io/dev/api/snaps/info/{snap_name}
232
+ """
233
+ snap_info = self.get_snap_info(session, snap_name)
234
+
235
+ return snap_info["snap_id"]
236
+
237
+ def snap_metadata(
238
+ self, session: dict, snap_id: str, json: Optional[dict] = None
239
+ ) -> dict:
240
+ """
241
+ Documentation:
242
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#managing-snap-metadata
243
+ Endpoint: [PUT]
244
+ https://dashboard.snapcraft.io/dev/api/snaps/{snap_id}/metadata
245
+ """
246
+ method = "PUT" if json is not None else None
247
+
248
+ metadata_response = self.session.request(
249
+ method=method,
250
+ url=self.get_endpoint_url(f"snaps/{snap_id}/metadata"),
251
+ params={"conflict_on_update": "true"},
252
+ headers=self._get_authorization_header(session),
253
+ json=json,
254
+ )
255
+
256
+ return self.process_response(metadata_response)
257
+
258
+ def snap_screenshots(
259
+ self,
260
+ session,
261
+ snap_id,
262
+ data: Optional[str] = None,
263
+ files: Optional[list] = None,
264
+ ) -> dict:
265
+ """
266
+ Documentation:
267
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#managing-snap-metadata
268
+ Endpoint: [GET, PUT]
269
+ https://dashboard.snapcraft.io/dev/api/snaps/{snap_id}/binary-metadata
270
+ """
271
+ method = "GET"
272
+ files_array = None
273
+ headers = self._get_authorization_header(session)
274
+ headers["Accept"] = "application/json"
275
+
276
+ if data:
277
+ method = "PUT"
278
+
279
+ files_array = []
280
+ if files:
281
+ for f in files:
282
+ files_array.append(
283
+ (f.filename, (f.filename, f.stream, f.mimetype))
284
+ )
285
+ else:
286
+ # API requires a multipart request, but we have no files to
287
+ # push https://github.com/requests/requests/issues/1081
288
+ # files_array.append(("info", ("", data["info"], "")))
289
+ data = None
290
+
291
+ screenshot_response = self.session.request(
292
+ method=method,
293
+ url=self.get_endpoint_url(f"snaps/{snap_id}/binary-metadata"),
294
+ params={"conflict_on_update": "true"},
295
+ headers=headers,
296
+ data=data,
297
+ files=files_array,
298
+ )
299
+
300
+ return self.process_response(screenshot_response)
301
+
302
+ def get_snap_revision(
303
+ self, session: dict, snap_id: str, revision_id: int
304
+ ) -> dict:
305
+ """
306
+ Documentation:
307
+ https://dashboard.snapcraft.io/docs/reference/v1/macaroon.html#request-a-macaroon
308
+ Endpoint: [GET]
309
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_id}/revisions/{revision_id}
310
+ """
311
+ response = self.session.get(
312
+ url=self.get_endpoint_url(
313
+ f"snaps/{snap_id}/revisions/{revision_id}", api_version=2
314
+ ),
315
+ headers=self._get_authorization_header(session),
316
+ )
317
+
318
+ return self.process_response(response)
319
+
320
+ def snap_release_history(
321
+ self, session: dict, snap_name: str, page: int = 1
322
+ ) -> dict:
323
+ """
324
+ Documentation:
325
+ https://dashboard.snapcraft.io/docs/reference/v2/en/snaps.html#snap-releases
326
+ Endpoint: [GET]
327
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_name}/releases
328
+ """
329
+ response = self.session.get(
330
+ url=self.get_endpoint_url(
331
+ f"snaps/{snap_name}/releases", api_version=2
332
+ ),
333
+ params={"page": page},
334
+ headers=self._get_authorization_header(session),
335
+ )
336
+
337
+ return self.process_response(response)
338
+
339
+ def snap_channel_map(self, session: dict, snap_name: str) -> dict:
340
+ """
341
+ Documentation:
342
+ https://dashboard.snapcraft.io/docs/reference/v2/en/snaps.html#snap-channel-map
343
+ Endpoint: [GET]
344
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_name}/channel-map
345
+ """
346
+ response = self.session.get(
347
+ url=self.get_endpoint_url(
348
+ f"snaps/{snap_name}/channel-map", api_version=2
349
+ ),
350
+ headers=self._get_authorization_header(session),
351
+ )
352
+
353
+ return self.process_response(response)
354
+
355
+ def post_snap_release(self, session: dict, json: dict) -> dict:
356
+ """
357
+ Documentation:
358
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#release-a-snap-build-to-a-channel
359
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/snap-release
360
+ """
361
+ response = self.session.post(
362
+ url=self.get_endpoint_url("snap-release/"),
363
+ headers=self._get_authorization_header(session),
364
+ json=json,
365
+ )
366
+
367
+ return self.process_response(response)
368
+
369
+ def post_close_channel(
370
+ self, session: dict, snap_id: str, json: dict
371
+ ) -> dict:
372
+ """
373
+ Documentation:
374
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#close-a-channel-for-a-snap-package
375
+ Endpoint: [POST]
376
+ https://dashboard.snapcraft.io/dev/api/snaps/{snap_id}/close
377
+ """
378
+ response = self.session.post(
379
+ url=self.get_endpoint_url(f"snaps/{snap_id}/close"),
380
+ headers=self._get_authorization_header(session),
381
+ json=json,
382
+ )
383
+
384
+ return self.process_response(response)
385
+
386
+ def get_publisher_metrics(self, session: dict, json: dict) -> dict:
387
+ """
388
+ Documentation:
389
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#fetch-metrics-for-snaps
390
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/snaps/metrics
391
+ """
392
+ headers = self._get_authorization_header(session)
393
+ headers["Content-Type"] = "application/json"
394
+
395
+ metrics_response = self.session.post(
396
+ url=self.get_endpoint_url("snaps/metrics"),
397
+ headers=headers,
398
+ json=json,
399
+ )
400
+
401
+ return self.process_response(metrics_response)
402
+
403
+ def get_validation_sets(self, session: dict) -> dict:
404
+ """
405
+ Return a list of validation sets for the current account
406
+ Documentation:
407
+ https://dashboard.snapcraft.io/docs/reference/v2/en/validation-sets.html
408
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/validation-sets
409
+ """
410
+ url = self.get_endpoint_url("validation-sets", api_version=2)
411
+ response = self.session.get(
412
+ url, headers=self._get_authorization_header(session)
413
+ )
414
+ return self.process_response(response)
415
+
416
+ def get_validation_set(
417
+ self, session: dict, validation_set_id: str
418
+ ) -> dict:
419
+ """
420
+ Return a validation set for the current account
421
+ Documentation:
422
+ https://dashboard.snapcraft.io/docs/reference/v2/en/validation-sets.html
423
+ Endpoint:
424
+ [GET] https://dashboard.snapcraft.io/api/v2/validation-sets/{id}
425
+ """
426
+ url = self.get_endpoint_url(
427
+ f"validation-sets/{validation_set_id}?sequence=all", api_version=2
428
+ )
429
+ response = self.session.get(
430
+ url, headers=self._get_authorization_header(session)
431
+ )
432
+ return self.process_response(response)
433
+
434
+ def get_stores(
435
+ self,
436
+ session: dict,
437
+ roles: List[str] = ["admin", "review", "view", "access"],
438
+ ) -> List[dict]:
439
+ """Return a list a stores with the given roles
440
+ Documentation:
441
+ https://dashboard.snapcraft.io/docs/reference/v1/account.html#get--dev-api-account
442
+ Endpoint: [GET] https://dashboard.snapcraft.io/dev/api/account
443
+
444
+ :return: A list of stores
445
+ """
446
+ headers = self._get_authorization_header(session)
447
+
448
+ response = self.session.get(
449
+ url=self.get_endpoint_url("account", 1), headers=headers
450
+ )
451
+
452
+ account_info = self.process_response(response)
453
+ stores = account_info.get("stores", [])
454
+ user_stores = []
455
+
456
+ for store in stores:
457
+ if not set(roles).isdisjoint(store["roles"]):
458
+ user_stores.append(store)
459
+
460
+ return user_stores
461
+
462
+ def get_store(self, session: dict, store_id: str) -> dict:
463
+ """Return a store where the user is an admin
464
+ Documentation:
465
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
466
+ Endpoint: [GET]
467
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}
468
+
469
+ :return: Store details
470
+ """
471
+ headers = self._get_authorization_header(session)
472
+
473
+ response = self.session.get(
474
+ url=self.get_endpoint_url(store_id, api_version=2, is_store=True),
475
+ headers=headers,
476
+ )
477
+
478
+ return self.process_response(response)["store"]
479
+
480
+ def get_store_snaps(
481
+ self,
482
+ session: dict,
483
+ store_id: str,
484
+ query: Optional[str] = None,
485
+ allowed_for_inclusion: Optional[str] = None,
486
+ ) -> List[dict]:
487
+ """
488
+ Documentation:
489
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#get
490
+ Endpoint: [GET]
491
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/snaps
492
+ """
493
+ headers = self._get_authorization_header(session)
494
+ params = {}
495
+
496
+ if query:
497
+ params["q"] = query
498
+
499
+ if allowed_for_inclusion:
500
+ params["allowed-for-inclusion"] = allowed_for_inclusion
501
+
502
+ response = self.session.get(
503
+ url=self.get_endpoint_url(
504
+ f"{store_id}/snaps", api_version=2, is_store=True
505
+ ),
506
+ params=params,
507
+ headers=headers,
508
+ )
509
+ return self.process_response(response).get("snaps", [])
510
+
511
+ def get_store_members(self, session: dict, store_id: str) -> List[dict]:
512
+ """
513
+ Documentation:
514
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
515
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/stores/{store_id}
516
+ """
517
+ headers = self._get_authorization_header(session)
518
+
519
+ response = self.session.get(
520
+ url=self.get_endpoint_url(
521
+ f"{store_id}", api_version=2, is_store=True
522
+ ),
523
+ headers=headers,
524
+ )
525
+
526
+ return self.process_response(response).get("users", [])
527
+
528
+ def update_store_members(
529
+ self, session: dict, store_id: str, members: dict
530
+ ) -> dict:
531
+ """
532
+ Documentation:
533
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#add-remove-or-edit-users-roles
534
+ Endpoint: [POST]
535
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/users
536
+ """
537
+ headers = self._get_authorization_header(session)
538
+
539
+ response = self.session.post(
540
+ url=self.get_endpoint_url(
541
+ f"{store_id}/users", api_version=2, is_store=True
542
+ ),
543
+ headers=headers,
544
+ json=members,
545
+ )
546
+
547
+ return self.process_response(response)
548
+
549
+ def invite_store_members(
550
+ self, session: dict, store_id: str, members: dict
551
+ ) -> dict:
552
+ """
553
+ Documentation:
554
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#manage-store-invitations
555
+ Endpoint: [POST]
556
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/invites
557
+ """
558
+ headers = self._get_authorization_header(session)
559
+
560
+ response = self.session.post(
561
+ url=self.get_endpoint_url(
562
+ f"{store_id}/invites", api_version=2, is_store=True
563
+ ),
564
+ headers=headers,
565
+ json=members,
566
+ )
567
+
568
+ return self.process_response(response)
569
+
570
+ def change_store_settings(
571
+ self, session: dict, store_id: str, settings: dict
572
+ ) -> dict:
573
+ """
574
+ Documentation:
575
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#change-store-settings
576
+ Endpoint: [PUT]
577
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/settings
578
+ """
579
+ headers = self._get_authorization_header(session)
580
+
581
+ response = self.session.put(
582
+ url=self.get_endpoint_url(
583
+ f"{store_id}/settings", api_version=2, is_store=True
584
+ ),
585
+ headers=headers,
586
+ json=settings,
587
+ )
588
+
589
+ return self.process_response(response)
590
+
591
+ def update_store_snaps(
592
+ self, session: dict, store_id: str, snaps: list
593
+ ) -> dict:
594
+ """
595
+ Documentation:
596
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#post
597
+ Endpoint: [POST]
598
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/snaps
599
+ """
600
+ headers = self._get_authorization_header(session)
601
+
602
+ response = self.session.post(
603
+ url=self.get_endpoint_url(
604
+ f"{store_id}/snaps", api_version=2, is_store=True
605
+ ),
606
+ headers=headers,
607
+ json=snaps,
608
+ )
609
+
610
+ return self.process_response(response)
611
+
612
+ def update_store_invites(
613
+ self, session: dict, store_id: str, invites: list
614
+ ) -> dict:
615
+ """
616
+ Documentation:
617
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#manage-store-invitations
618
+ Endpoint: [PUT]
619
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/invites
620
+ """
621
+ headers = self._get_authorization_header(session)
622
+
623
+ response = self.session.put(
624
+ url=self.get_endpoint_url(
625
+ f"{store_id}/invites", api_version=2, is_store=True
626
+ ),
627
+ headers=headers,
628
+ json=invites,
629
+ )
630
+
631
+ return self.process_response(response)
632
+
633
+ def get_store_invites(self, session: dict, store_id: str) -> List[dict]:
634
+ """
635
+ Documentation:
636
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
637
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/stores/{store_id}
638
+ """
639
+ headers = self._get_authorization_header(session)
640
+
641
+ response = self.session.get(
642
+ url=self.get_endpoint_url(
643
+ f"{store_id}", api_version=2, is_store=True
644
+ ),
645
+ headers=headers,
646
+ )
647
+ return self.process_response(response).get("invites", [])