canonicalwebteam.store-api 5.0.0__py3-none-any.whl → 6.1.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,646 @@
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
+ files_array.append(("info", ("", data["info"], "")))
288
+ data = None
289
+
290
+ screenshot_response = self.session.request(
291
+ method=method,
292
+ url=self.get_endpoint_url(f"snaps/{snap_id}/binary-metadata"),
293
+ params={"conflict_on_update": "true"},
294
+ headers=headers,
295
+ data=data,
296
+ files=files_array,
297
+ )
298
+
299
+ return self.process_response(screenshot_response)
300
+
301
+ def get_snap_revision(
302
+ self, session: dict, snap_id: str, revision_id: int
303
+ ) -> dict:
304
+ """
305
+ Documentation:
306
+ https://dashboard.snapcraft.io/docs/reference/v1/macaroon.html#request-a-macaroon
307
+ Endpoint: [GET]
308
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_id}/revisions/{revision_id}
309
+ """
310
+ response = self.session.get(
311
+ url=self.get_endpoint_url(
312
+ f"snaps/{snap_id}/revisions/{revision_id}", api_version=2
313
+ ),
314
+ headers=self._get_authorization_header(session),
315
+ )
316
+
317
+ return self.process_response(response)
318
+
319
+ def snap_release_history(
320
+ self, session: dict, snap_name: str, page: int = 1
321
+ ) -> dict:
322
+ """
323
+ Documentation:
324
+ https://dashboard.snapcraft.io/docs/reference/v2/en/snaps.html#snap-releases
325
+ Endpoint: [GET]
326
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_name}/releases
327
+ """
328
+ response = self.session.get(
329
+ url=self.get_endpoint_url(
330
+ f"snaps/{snap_name}/releases", api_version=2
331
+ ),
332
+ params={"page": page},
333
+ headers=self._get_authorization_header(session),
334
+ )
335
+
336
+ return self.process_response(response)
337
+
338
+ def snap_channel_map(self, session: dict, snap_name: str) -> dict:
339
+ """
340
+ Documentation:
341
+ https://dashboard.snapcraft.io/docs/reference/v2/en/snaps.html#snap-channel-map
342
+ Endpoint: [GET]
343
+ https://dashboard.snapcraft.io/api/v2/snaps/{snap_name}/channel-map
344
+ """
345
+ response = self.session.get(
346
+ url=self.get_endpoint_url(
347
+ f"snaps/{snap_name}/channel-map", api_version=2
348
+ ),
349
+ headers=self._get_authorization_header(session),
350
+ )
351
+
352
+ return self.process_response(response)
353
+
354
+ def post_snap_release(self, session: dict, json: dict) -> dict:
355
+ """
356
+ Documentation:
357
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#release-a-snap-build-to-a-channel
358
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/snap-release
359
+ """
360
+ response = self.session.post(
361
+ url=self.get_endpoint_url("snap-release/"),
362
+ headers=self._get_authorization_header(session),
363
+ json=json,
364
+ )
365
+
366
+ return self.process_response(response)
367
+
368
+ def post_close_channel(
369
+ self, session: dict, snap_id: str, json: dict
370
+ ) -> dict:
371
+ """
372
+ Documentation:
373
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#close-a-channel-for-a-snap-package
374
+ Endpoint: [POST]
375
+ https://dashboard.snapcraft.io/dev/api/snaps/{snap_id}/close
376
+ """
377
+ response = self.session.post(
378
+ url=self.get_endpoint_url(f"snaps/{snap_id}/close"),
379
+ headers=self._get_authorization_header(session),
380
+ json=json,
381
+ )
382
+
383
+ return self.process_response(response)
384
+
385
+ def get_publisher_metrics(self, session: dict, json: dict) -> dict:
386
+ """
387
+ Documentation:
388
+ https://dashboard.snapcraft.io/docs/reference/v1/snap.html#fetch-metrics-for-snaps
389
+ Endpoint: [POST] https://dashboard.snapcraft.io/dev/api/snaps/metrics
390
+ """
391
+ headers = self._get_authorization_header(session)
392
+ headers["Content-Type"] = "application/json"
393
+
394
+ metrics_response = self.session.post(
395
+ url=self.get_endpoint_url("snaps/metrics"),
396
+ headers=headers,
397
+ json=json,
398
+ )
399
+
400
+ return self.process_response(metrics_response)
401
+
402
+ def get_validation_sets(self, session: dict) -> dict:
403
+ """
404
+ Return a list of validation sets for the current account
405
+ Documentation:
406
+ https://dashboard.snapcraft.io/docs/reference/v2/en/validation-sets.html
407
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/validation-sets
408
+ """
409
+ url = self.get_endpoint_url("validation-sets", api_version=2)
410
+ response = self.session.get(
411
+ url, headers=self._get_authorization_header(session)
412
+ )
413
+ return self.process_response(response)
414
+
415
+ def get_validation_set(
416
+ self, session: dict, validation_set_id: str
417
+ ) -> dict:
418
+ """
419
+ Return a validation set for the current account
420
+ Documentation:
421
+ https://dashboard.snapcraft.io/docs/reference/v2/en/validation-sets.html
422
+ Endpoint:
423
+ [GET] https://dashboard.snapcraft.io/api/v2/validation-sets/{id}
424
+ """
425
+ url = self.get_endpoint_url(
426
+ f"validation-sets/{validation_set_id}?sequence=all", api_version=2
427
+ )
428
+ response = self.session.get(
429
+ url, headers=self._get_authorization_header(session)
430
+ )
431
+ return self.process_response(response)
432
+
433
+ def get_stores(
434
+ self,
435
+ session: dict,
436
+ roles: List[str] = ["admin", "review", "view", "access"],
437
+ ) -> List[dict]:
438
+ """Return a list a stores with the given roles
439
+ Documentation:
440
+ https://dashboard.snapcraft.io/docs/reference/v1/account.html#get--dev-api-account
441
+ Endpoint: [GET] https://dashboard.snapcraft.io/dev/api/account
442
+
443
+ :return: A list of stores
444
+ """
445
+ headers = self._get_authorization_header(session)
446
+
447
+ response = self.session.get(
448
+ url=self.get_endpoint_url("account", 1), headers=headers
449
+ )
450
+
451
+ account_info = self.process_response(response)
452
+ stores = account_info.get("stores", [])
453
+ user_stores = []
454
+
455
+ for store in stores:
456
+ if not set(roles).isdisjoint(store["roles"]):
457
+ user_stores.append(store)
458
+
459
+ return user_stores
460
+
461
+ def get_store(self, session: dict, store_id: str) -> dict:
462
+ """Return a store where the user is an admin
463
+ Documentation:
464
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
465
+ Endpoint: [GET]
466
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}
467
+
468
+ :return: Store details
469
+ """
470
+ headers = self._get_authorization_header(session)
471
+
472
+ response = self.session.get(
473
+ url=self.get_endpoint_url(store_id, api_version=2, is_store=True),
474
+ headers=headers,
475
+ )
476
+
477
+ return self.process_response(response)["store"]
478
+
479
+ def get_store_snaps(
480
+ self,
481
+ session: dict,
482
+ store_id: str,
483
+ query: Optional[str] = None,
484
+ allowed_for_inclusion: Optional[str] = None,
485
+ ) -> List[dict]:
486
+ """
487
+ Documentation:
488
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#get
489
+ Endpoint: [GET]
490
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/snaps
491
+ """
492
+ headers = self._get_authorization_header(session)
493
+ params = {}
494
+
495
+ if query:
496
+ params["q"] = query
497
+
498
+ if allowed_for_inclusion:
499
+ params["allowed-for-inclusion"] = allowed_for_inclusion
500
+
501
+ response = self.session.get(
502
+ url=self.get_endpoint_url(
503
+ f"{store_id}/snaps", api_version=2, is_store=True
504
+ ),
505
+ params=params,
506
+ headers=headers,
507
+ )
508
+ return self.process_response(response).get("snaps", [])
509
+
510
+ def get_store_members(self, session: dict, store_id: str) -> List[dict]:
511
+ """
512
+ Documentation:
513
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
514
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/stores/{store_id}
515
+ """
516
+ headers = self._get_authorization_header(session)
517
+
518
+ response = self.session.get(
519
+ url=self.get_endpoint_url(
520
+ f"{store_id}", api_version=2, is_store=True
521
+ ),
522
+ headers=headers,
523
+ )
524
+
525
+ return self.process_response(response).get("users", [])
526
+
527
+ def update_store_members(
528
+ self, session: dict, store_id: str, members: dict
529
+ ) -> dict:
530
+ """
531
+ Documentation:
532
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#add-remove-or-edit-users-roles
533
+ Endpoint: [POST]
534
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/users
535
+ """
536
+ headers = self._get_authorization_header(session)
537
+
538
+ response = self.session.post(
539
+ url=self.get_endpoint_url(
540
+ f"{store_id}/users", api_version=2, is_store=True
541
+ ),
542
+ headers=headers,
543
+ json=members,
544
+ )
545
+
546
+ return self.process_response(response)
547
+
548
+ def invite_store_members(
549
+ self, session: dict, store_id: str, members: dict
550
+ ) -> dict:
551
+ """
552
+ Documentation:
553
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#manage-store-invitations
554
+ Endpoint: [POST]
555
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/invites
556
+ """
557
+ headers = self._get_authorization_header(session)
558
+
559
+ response = self.session.post(
560
+ url=self.get_endpoint_url(
561
+ f"{store_id}/invites", api_version=2, is_store=True
562
+ ),
563
+ headers=headers,
564
+ json=members,
565
+ )
566
+
567
+ return self.process_response(response)
568
+
569
+ def change_store_settings(
570
+ self, session: dict, store_id: str, settings: dict
571
+ ) -> dict:
572
+ """
573
+ Documentation:
574
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#change-store-settings
575
+ Endpoint: [PUT]
576
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/settings
577
+ """
578
+ headers = self._get_authorization_header(session)
579
+
580
+ response = self.session.put(
581
+ url=self.get_endpoint_url(
582
+ f"{store_id}/settings", api_version=2, is_store=True
583
+ ),
584
+ headers=headers,
585
+ json=settings,
586
+ )
587
+
588
+ return self.process_response(response)
589
+
590
+ def update_store_snaps(
591
+ self, session: dict, store_id: str, snaps: list
592
+ ) -> dict:
593
+ """
594
+ Documentation:
595
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#post
596
+ Endpoint: [POST]
597
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/snaps
598
+ """
599
+ headers = self._get_authorization_header(session)
600
+
601
+ response = self.session.post(
602
+ url=self.get_endpoint_url(
603
+ f"{store_id}/snaps", api_version=2, is_store=True
604
+ ),
605
+ headers=headers,
606
+ json=snaps,
607
+ )
608
+
609
+ return self.process_response(response)
610
+
611
+ def update_store_invites(
612
+ self, session: dict, store_id: str, invites: list
613
+ ) -> dict:
614
+ """
615
+ Documentation:
616
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#manage-store-invitations
617
+ Endpoint: [PUT]
618
+ https://dashboard.snapcraft.io/api/v2/stores/{store_id}/invites
619
+ """
620
+ headers = self._get_authorization_header(session)
621
+
622
+ response = self.session.put(
623
+ url=self.get_endpoint_url(
624
+ f"{store_id}/invites", api_version=2, is_store=True
625
+ ),
626
+ headers=headers,
627
+ json=invites,
628
+ )
629
+
630
+ return self.process_response(response)
631
+
632
+ def get_store_invites(self, session: dict, store_id: str) -> List[dict]:
633
+ """
634
+ Documentation:
635
+ https://dashboard.snapcraft.io/docs/reference/v2/en/stores.html#list-the-details-of-a-brand-store
636
+ Endpoint: [GET] https://dashboard.snapcraft.io/api/v2/stores/{store_id}
637
+ """
638
+ headers = self._get_authorization_header(session)
639
+
640
+ response = self.session.get(
641
+ url=self.get_endpoint_url(
642
+ f"{store_id}", api_version=2, is_store=True
643
+ ),
644
+ headers=headers,
645
+ )
646
+ return self.process_response(response).get("invites", [])