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,832 @@
1
+ from os import getenv
2
+ from typing import Optional
3
+ from requests import Session
4
+
5
+ from canonicalwebteam.store_api.base import Base
6
+ from canonicalwebteam.store_api.dashboard import Dashboard
7
+
8
+ PUBLISHERGW_URL = getenv("PUBLISHERGW_URL", "https://api.charmhub.io")
9
+ VALID_NAMESPACE = ["charm", "snap"]
10
+ CHARMSTORE_VALID_PACKAGE_TYPES = ["charm", "bundle"]
11
+ dashboard_authorization_header = Dashboard()._get_authorization_header
12
+
13
+
14
+ class PublisherGW(Base):
15
+ def __init__(self, name_space: str, session=Session()):
16
+ super().__init__(session)
17
+ self.name_space = name_space
18
+ self.config = {
19
+ 1: {"base_url": f"{PUBLISHERGW_URL}/v1"},
20
+ 2: {"base_url": f"{PUBLISHERGW_URL}/v2"},
21
+ }
22
+
23
+ def get_endpoint_url(
24
+ self, endpoint: str, version: int = 1, has_name_space: bool = False
25
+ ) -> str:
26
+ base_url = self.config[version]["base_url"]
27
+ if has_name_space:
28
+ return f"{base_url}/{self.name_space}/{endpoint}".rstrip("/")
29
+ return f"{base_url}/{endpoint}".rstrip("/")
30
+
31
+ # SEARCH
32
+ def find(
33
+ self,
34
+ query: str = "",
35
+ category: str = "",
36
+ publisher: str = "",
37
+ type: Optional[str] = None,
38
+ provides: list = [],
39
+ requires: list = [],
40
+ fields: list = [],
41
+ ) -> dict:
42
+ """
43
+ Given a search term, return an array of matching search results.
44
+ v2 API only.
45
+
46
+ Documentation: https://api.snapcraft.io/docs/charms.html#charm_find
47
+ Endpoint: https://api.charmhub.io/v2/{name_space}/find
48
+ """
49
+ url = self.get_endpoint_url(f"{self.name_space}s/find", 2)
50
+ headers = self.config[2].get("headers")
51
+ params = {
52
+ "q": query,
53
+ "category": category,
54
+ "publisher": publisher,
55
+ "type": type,
56
+ }
57
+ if fields:
58
+ params["fields"] = ",".join(fields)
59
+
60
+ if provides:
61
+ params["provides"] = ",".join(provides)
62
+
63
+ if requires:
64
+ params["requires"] = ",".join(requires)
65
+
66
+ return self.process_response(
67
+ self.session.get(url, params=params, headers=headers)
68
+ )
69
+
70
+ # CATEGORIES
71
+ def get_categories(
72
+ self, api_version: int = 2, type: str = "shared"
73
+ ) -> dict:
74
+ """
75
+ Documentation: https://api.snapcraft.io/docs/categories.html
76
+ Endpoint: https://api.charmhub.io/v2/{name_space}/categories
77
+ """
78
+ url = self.get_endpoint_url("charms/categories", api_version)
79
+ return self.process_response(
80
+ self.session.get(
81
+ url,
82
+ headers=self.config[api_version].get("headers"),
83
+ params={"type": type},
84
+ )
85
+ )
86
+
87
+ # AUTH AND MACAROONS
88
+
89
+ def _get_authorization_header(self, session: str) -> dict:
90
+ """
91
+ Return the formatted Authorization header for the publisher API.
92
+ """
93
+ return {"Authorization": f"Macaroon {session}"}
94
+
95
+ def _get_dev_token_authorization_header(self, session: dict):
96
+ return {"Authorization": f"Macaroon {session['developer_token']}"}
97
+
98
+ def get_macaroon(self) -> str:
99
+ """
100
+ Return existing macaroons for the authenticated account.
101
+ Documentation: https://api.charmhub.io/docs/default.html#get_macaroon
102
+ Endpoint URL: [GET] https://api.charmhub.io/v1/tokens
103
+ """
104
+ response = self.session.get(url=self.get_endpoint_url("tokens"))
105
+ return self.process_response(response)["macaroon"]
106
+
107
+ def issue_macaroon(
108
+ self,
109
+ permissions: list,
110
+ description: Optional[list] = None,
111
+ ttl: Optional[list] = None,
112
+ ) -> str:
113
+ """
114
+ Return a bakery v2 macaroon to be discharged by Candid.
115
+ Documentation: https://api.charmhub.io/docs/default.html#issue_macaroon
116
+ Endpoint URL: [POST] https://api.charmhub.io/v1/tokens
117
+ """
118
+ data = {"permissions": permissions}
119
+
120
+ if description:
121
+ data["description"] = description
122
+
123
+ if ttl:
124
+ data["ttl"] = ttl
125
+
126
+ response = self.session.post(
127
+ url=self.get_endpoint_url("tokens"),
128
+ json=data,
129
+ )
130
+ return self.process_response(response)["macaroon"]
131
+
132
+ def exchange_macaroons(self, issued_macaroon: str) -> str:
133
+ """
134
+ Return an exchanged snapstore-only authentication macaroon.
135
+ Documentation:
136
+ https://api.charmhub.io/docs/default.html#exchange_macaroons
137
+ Endpoint URL: [POST] https://api.charmhub.io/v1/tokens/exchange
138
+ """
139
+
140
+ response = self.session.post(
141
+ url=self.get_endpoint_url("tokens/exchange"),
142
+ headers={"Macaroons": issued_macaroon},
143
+ json={},
144
+ )
145
+
146
+ return self.process_response(response)["macaroon"]
147
+
148
+ def exchange_dashboard_macaroons(self, session: dict) -> str:
149
+ """
150
+ Exchange dashboard.snapcraft.io SSO discharged macaroons
151
+ Documentation:
152
+ https://api.charmhub.io/docs/default.html#exchange_dashboard_macaroons
153
+ Endpoint: [POST] https://api.charmhub.io/v1/tokens/dashboard/exchange
154
+ """
155
+ url = self.get_endpoint_url("tokens/dashboard/exchange")
156
+ response = self.session.post(
157
+ url=url,
158
+ headers=dashboard_authorization_header(session),
159
+ json={},
160
+ )
161
+ return self.process_response(response)["macaroon"]
162
+
163
+ def macaroon_info(self, publisher_auth: str) -> dict:
164
+ """
165
+ Return information about the authenticated macaroon token.
166
+ Documentation: https://api.charmhub.io/docs/default.html#macaroon_info
167
+ Endpoint URL: [GET] https://api.charmhub.io/v1/tokens/whoami
168
+ """
169
+ response = self.session.get(
170
+ url=self.get_endpoint_url("tokens/whoami"),
171
+ headers=self._get_authorization_header(publisher_auth),
172
+ )
173
+
174
+ return self.process_response(response)
175
+
176
+ # PACKAGES MANAGEMENT
177
+ def get_account_packages(
178
+ self,
179
+ publisher_auth: str,
180
+ package_type: str,
181
+ include_collaborations: bool = False,
182
+ status: Optional[str] = None,
183
+ ):
184
+ """
185
+ Return publisher packages
186
+ Documentation: https://api.charmhub.io/docs/default.html
187
+ Endpoint URL: [GET] https://api.charmhub.io/v1/{package_type}
188
+
189
+ Args:
190
+ publisher_auth: Serialized macaroon to consume the API.
191
+ package_type: Type of packages to obtain.
192
+ include_collaborations (optional): Include shared charms
193
+ status (optional): Only packages with the given status
194
+
195
+ Returns:
196
+ A list of packages
197
+ """
198
+
199
+ if (
200
+ self.name_space == "charms"
201
+ and package_type not in CHARMSTORE_VALID_PACKAGE_TYPES
202
+ ):
203
+ raise ValueError(
204
+ "Invalid package type. Expected one of: %s"
205
+ % CHARMSTORE_VALID_PACKAGE_TYPES
206
+ )
207
+
208
+ params = {}
209
+
210
+ if include_collaborations:
211
+ params["include-collaborations"] = "true"
212
+ response = self.session.get(
213
+ url=self.get_endpoint_url(package_type),
214
+ headers=self._get_authorization_header(publisher_auth),
215
+ params=params,
216
+ )
217
+ packages = self.process_response(response)["results"]
218
+
219
+ if status:
220
+ packages = [p for p in packages if p["status"] == status]
221
+
222
+ return packages
223
+
224
+ def get_package_metadata(self, session: dict, package_name: str) -> dict:
225
+ """
226
+ Get general metadata for a package.
227
+ Documentation:
228
+ https://api.charmhub.io/docs/default.html#package_metadata
229
+ Endpoint URL: [GET]
230
+ https://api.charmhub.io/v1/{name_space}/{package_name}
231
+ namespace: charm for both charms and bundles, snap for snaps
232
+ package_name: Package name
233
+
234
+ Args:
235
+ publisher_auth: Serialized macaroon to consume the API.
236
+ package_type: Type of packages to obtain.
237
+
238
+ Returns:
239
+ Package general metadata
240
+ """
241
+ response = self.session.get(
242
+ url=self.get_endpoint_url(f"{package_name}", has_name_space=True),
243
+ headers=self._get_dev_token_authorization_header(session),
244
+ )
245
+
246
+ return self.process_response(response)["metadata"]
247
+
248
+ def update_package_metadata(
249
+ self, publisher_auth: str, package_type: str, name: str, data: dict
250
+ ) -> dict:
251
+ """
252
+ Update general metadata for a package.
253
+ Documentation:
254
+ https://api.charmhub.io/docs/default.html#update_package_metadata
255
+ Endpoint URL: [PATCH]
256
+ https://api.charmhub.io/v1/{namespace}/<name>
257
+ namespace: charm for both charms and bundles
258
+ name: Package name
259
+
260
+ Args:
261
+ publisher_auth: Serialized macaroon to consume the API.
262
+ package_type: Type of packages to obtain.
263
+ name: Package name
264
+ data: Dict with changes to apply
265
+
266
+ Returns:
267
+ Package general metadata with changes applied
268
+ """
269
+
270
+ if (
271
+ self.name_space == "charm"
272
+ and package_type not in CHARMSTORE_VALID_PACKAGE_TYPES
273
+ ):
274
+ raise ValueError(
275
+ "Invalid package type. Expected one of: %s"
276
+ % CHARMSTORE_VALID_PACKAGE_TYPES
277
+ )
278
+
279
+ response = self.session.patch(
280
+ url=self.get_endpoint_url(f"{package_type}/{name}"),
281
+ headers=self._get_authorization_header(publisher_auth),
282
+ json=data,
283
+ )
284
+
285
+ return self.process_response(response)["metadata"]
286
+
287
+ def register_package_name(self, publisher_auth: str, data: dict) -> dict:
288
+ """
289
+ Register a package name.
290
+ Documentation: https://api.charmhub.io/docs/default.html#register_name
291
+ Endpoint URL: [POST] https://api.charmhub.io/v1/{namespace}
292
+
293
+ Args:
294
+ publisher_auth: Serialized macaroon to consume the API.
295
+ data: Dict with name, type and visibility of the package
296
+
297
+ Returns:
298
+ Newly registered name id
299
+ """
300
+
301
+ response = self.session.post(
302
+ url=self.get_endpoint_url("", has_name_space=True),
303
+ headers=self._get_authorization_header(publisher_auth),
304
+ json=data,
305
+ )
306
+
307
+ return self.process_response(response)
308
+
309
+ def unregister_package_name(
310
+ self, session: dict, package_name: str
311
+ ) -> dict:
312
+ """
313
+ Unregister a package name.
314
+ Documentation:
315
+ https://api.charmhub.io/docs/default.html#unregister_package
316
+ Endpoint URL: [DELETE] https://api.charmhub.io/v1/charm/<name>
317
+
318
+ Args:
319
+ publisher_auth: Serialized macaroon to consume the API.
320
+ name: Name of the package to unregister
321
+ Returns:
322
+ The package name ID if successful
323
+ Otherwise, returns an error list
324
+ """
325
+ url = self.get_endpoint_url(package_name, has_name_space=True)
326
+ response = self.session.delete(
327
+ url=url,
328
+ headers=dashboard_authorization_header(session),
329
+ )
330
+ return response
331
+
332
+ def get_charm_libraries(self, package_name: str) -> dict:
333
+ """
334
+ Get libraries for a charm.
335
+ Documentation:
336
+ https://api.charmhub.io/docs/libraries.html#fetch_libraries
337
+ Endpoint URL: [POST]
338
+ https://api.charmhub.io/v1/{name_space}/libraries/bulk
339
+ """
340
+
341
+ response = self.session.post(
342
+ url=self.get_endpoint_url("libraries/bulk", has_name_space=True),
343
+ json=[{"charm-name": package_name}],
344
+ )
345
+
346
+ return self.process_response(response)
347
+
348
+ def get_charm_library(
349
+ self,
350
+ charm_name: str,
351
+ library_id: str,
352
+ api_version: Optional[int] = None,
353
+ ) -> dict:
354
+ """
355
+ Get library metadata and content
356
+ Documentation:
357
+ https://api.charmhub.io/docs/libraries.html#fetch_library
358
+ Endpoint URL: [GET]
359
+ https://api.charmhub.io/v1/charm/libraries/{charm_name}/{library_id}
360
+
361
+ Args:
362
+ charm_name: Name of the charm
363
+ library_id: ID of the library
364
+ api_version: API version to use
365
+ """
366
+ params = {}
367
+
368
+ if api_version is not None:
369
+ params["api"] = api_version
370
+ response = self.session.get(
371
+ url=self.get_endpoint_url(
372
+ f"charm/libraries/{charm_name}/{library_id}"
373
+ ),
374
+ params=params,
375
+ )
376
+
377
+ return self.process_response(response)
378
+
379
+ def get_releases(self, publisher_auth: str, package_name: str) -> dict:
380
+ """
381
+ List of all releases for a package.
382
+ Documentation:
383
+ https://api.charmhub.io/docs/default.html#list_releases
384
+ Endpoint URL: [GET]
385
+ https://api.charmhub.io/v1/{namespace}/<name>/releases
386
+
387
+ Args:
388
+ publisher_auth: Serialized macaroon to consume the API.
389
+ name: Name of the package
390
+ """
391
+ response = self.session.get(
392
+ url=self.get_endpoint_url(
393
+ f"{package_name}/releases", has_name_space=True
394
+ ),
395
+ headers=self._get_authorization_header(publisher_auth),
396
+ )
397
+ return self.process_response(response)
398
+
399
+ def get_item_details(
400
+ self,
401
+ name: str,
402
+ channel: Optional[str] = None,
403
+ fields: list = [],
404
+ api_version: int = 2,
405
+ ) -> dict:
406
+ """
407
+ Documentation: https://api.snapcraft.io/docs/info.html
408
+ Endpoint: [GET]
409
+ https://api.charmhub.io/api/v2/{name_space}/info/{package_name}
410
+ """
411
+ url = self.get_endpoint_url(
412
+ f"{self.name_space}s/info/{name}", api_version
413
+ )
414
+ params = {}
415
+ if fields:
416
+ params["fields"] = ",".join(fields)
417
+ if channel:
418
+ params["channel"] = channel
419
+ headers = self.config[api_version].get("headers")
420
+ return self.process_response(
421
+ self.session.get(
422
+ url,
423
+ params=params,
424
+ headers=headers,
425
+ )
426
+ )
427
+
428
+ # COLLABORATORS
429
+ def get_collaborators(
430
+ self, publisher_auth: str, package_name: str
431
+ ) -> dict:
432
+ """
433
+ Get collaborators (accepted invites) for the given package.
434
+ Documentation:
435
+ https://api.charmhub.io/docs/collaborator.html#get_collaborators
436
+ Endpoint URL: [GET]
437
+ https://api.charmhub.io/v1/{name_space}/{package_name}/collaborators
438
+
439
+ Args:
440
+ publisher_auth: Serialized macaroon to consume the API.
441
+ name_space: Namespace of the package, can be 'snap' or 'charm'.
442
+ package_name: Name of the package
443
+ """
444
+ response = self.session.get(
445
+ url=self.get_endpoint_url(
446
+ f"{package_name}/collaborators", has_name_space=True
447
+ ),
448
+ headers=self._get_authorization_header(publisher_auth),
449
+ )
450
+ return self.process_response(response)
451
+
452
+ def get_pending_invites(
453
+ self, publisher_auth: str, package_name: str
454
+ ) -> dict:
455
+ """
456
+ Get pending collaborator invites for the given package.
457
+ Documentation:
458
+ https://api.charmhub.io/docs/collaborator.html#get_pending_invites
459
+ Endpoint URL: [GET]
460
+ https://api.charmhub.io/v1/{name_space}/{package_name}/collaborators/invites
461
+
462
+ Args:
463
+ publisher_auth: Serialized macaroon to consume the API.
464
+ name_space: Namespace of the package, can be 'snap' or 'charm'.
465
+ package_name: Name of the package
466
+ """
467
+ response = self.session.get(
468
+ url=self.get_endpoint_url(
469
+ f"{package_name}/collaborators/invites", has_name_space=True
470
+ ),
471
+ headers=self._get_authorization_header(publisher_auth),
472
+ )
473
+ return self.process_response(response)
474
+
475
+ def invite_collaborators(
476
+ self, publisher_auth: str, package_name: str, emails: list
477
+ ) -> dict:
478
+ """
479
+ Invite one or more collaborators for a package.
480
+ Documentation:
481
+ https://api.charmhub.io/docs/collaborator.html#invite_collaborators
482
+ Endpoint URL: [POST]
483
+ https://api.charmhub.io/v1/{name_space}/{package_name}/collaborators/invites
484
+
485
+ Args:
486
+ publisher_auth: Serialized macaroon to consume the API.
487
+ name_space: Namespace of the package, can be 'snap' or 'charm'.
488
+ package_name: Name of the package
489
+ emails: List of emails to invite
490
+ """
491
+ payload: dict = {"invites": []}
492
+
493
+ for email in emails:
494
+ payload["invites"].append({"email": email})
495
+
496
+ response = self.session.post(
497
+ url=self.get_endpoint_url(
498
+ f"{package_name}/collaborators/invites", has_name_space=True
499
+ ),
500
+ headers=self._get_authorization_header(publisher_auth),
501
+ json=payload,
502
+ )
503
+ return self.process_response(response)
504
+
505
+ def revoke_invites(
506
+ self, publisher_auth: str, package_name: str, emails: list
507
+ ) -> dict:
508
+ """
509
+ Revoke invites to the specified emails for the package.
510
+ Documentation:
511
+ https://api.charmhub.io/docs/collaborator.html#revoke_invites
512
+ Endpoint URL: [POST]
513
+ https://api.charmhub.io/v1/{namespace}/{package_name}/collaborators/invites/revoke
514
+
515
+ Args:
516
+ publisher_auth: Serialized macaroon to consume the API.
517
+ name: Name of the package
518
+ emails: List of emails to revoke
519
+ """
520
+ payload: dict = {"invites": []}
521
+
522
+ for email in emails:
523
+ payload["invites"].append({"email": email})
524
+
525
+ response = self.session.post(
526
+ url=self.get_endpoint_url(
527
+ f"{package_name}/collaborators/invites/revoke",
528
+ has_name_space=True,
529
+ ),
530
+ headers=self._get_authorization_header(publisher_auth),
531
+ json=payload,
532
+ )
533
+ return response
534
+
535
+ def accept_invite(
536
+ self, publisher_auth: str, package_name: str, token: str
537
+ ) -> dict:
538
+ """
539
+ Accept a collaborator invite.
540
+ Documentation:
541
+ https://api.charmhub.io/docs/collaborator.html#accept_invite
542
+ Endpoint URL: [POST]
543
+ https://api.charmhub.io/v1/{name_space}/{package_name}/collaborators/invites/accept
544
+
545
+ Args:
546
+ publisher_auth: Serialized macaroon to consume the API.
547
+ name: Name of the package
548
+ token: Invite token
549
+ """
550
+ response = self.session.post(
551
+ url=self.get_endpoint_url(
552
+ f"{package_name}/collaborators/invites/accept",
553
+ has_name_space=True,
554
+ ),
555
+ headers=self._get_authorization_header(publisher_auth),
556
+ json={"token": token},
557
+ )
558
+ return response
559
+
560
+ def reject_invite(
561
+ self, publisher_auth: str, package_name: str, token: str
562
+ ) -> dict:
563
+ """
564
+ Reject a collaborator invite.
565
+ Documentation:
566
+ https://api.charmhub.io/docs/collaborator.html#reject_invite
567
+ Endpoint URL: [POST]
568
+ https://api.charmhub.io/v1/{name_space}/{package_name}/collaborators/invites/reject
569
+
570
+ Args:
571
+ publisher_auth: Serialized macaroon to consume the API.
572
+ name: Name of the package
573
+ token: Invite token
574
+ """
575
+ response = self.session.post(
576
+ url=self.get_endpoint_url(
577
+ f"{package_name}/collaborators/invites/reject"
578
+ ),
579
+ headers=self._get_authorization_header(publisher_auth),
580
+ json={"token": token},
581
+ )
582
+ return response
583
+
584
+ # TRACKS
585
+ def create_track(
586
+ self,
587
+ session: dict,
588
+ package_name: str,
589
+ track_name: str,
590
+ version_pattern: Optional[str] = None,
591
+ auto_phasing_percentage: Optional[str] = None,
592
+ ) -> dict:
593
+ """
594
+ Create a track for an artefact base on the artefact's guardrail
595
+ pattern.
596
+ Documentation: https://api.charmhub.io/docs/default.html#create_tracks
597
+ Endpoint URL: [POST]
598
+ https://api.charmhub.io/v1/charm/{package_name}/tracks
599
+
600
+ Args:
601
+ publisher_auth: Serialized macaroon to consume the API.
602
+ charm_name: Name of the charm
603
+ track_name: Name of the track
604
+ version_pattern: Version pattern for the track (optional)
605
+ auto_phasing_percentage: phasing percentage for track (optional)
606
+ """
607
+
608
+ payload = {
609
+ "name": track_name,
610
+ "version-pattern": version_pattern,
611
+ "automatic-phasing-percentage": auto_phasing_percentage,
612
+ }
613
+ response = self.session.post(
614
+ url=self.get_endpoint_url(
615
+ f"{package_name}/tracks", has_name_space=True
616
+ ),
617
+ headers=self._get_dev_token_authorization_header(session),
618
+ json=[payload],
619
+ )
620
+ return response
621
+
622
+ # MODEL SERVICE ADMIN
623
+ def get_store_models(self, session: dict, store_id: str) -> dict:
624
+ """
625
+ Documentation:
626
+ https://api.charmhub.io/docs/model-service-admin.html#read_models
627
+ Endpoint: [GET] https://api.charmhub.io/v1/brand/{store_id}/model
628
+ """
629
+ response = self.session.get(
630
+ url=self.get_endpoint_url(f"brand/{store_id}/model"),
631
+ headers=self._get_dev_token_authorization_header(session),
632
+ )
633
+
634
+ return self.process_response(response)
635
+
636
+ def create_store_model(
637
+ self,
638
+ session: dict,
639
+ store_id: str,
640
+ name: str,
641
+ api_key: Optional[str] = None,
642
+ ) -> dict:
643
+ """
644
+ Documentation:
645
+ https://api.charmhub.io/docs/model-service-admin.html#create_model
646
+ Endpoint: [POST] https://api.charmhub.io/v1/brand/{store_id}/model
647
+ """
648
+ if api_key:
649
+ payload = {"name": name, "api-key": api_key, "series": "16"}
650
+ else:
651
+ payload = {"name": name, "series": "16"}
652
+ response = self.session.post(
653
+ url=self.get_endpoint_url(f"brand/{store_id}/model"),
654
+ headers=self._get_dev_token_authorization_header(session),
655
+ json=payload,
656
+ )
657
+
658
+ return self.process_response(response)
659
+
660
+ def update_store_model(
661
+ self, session: dict, store_id: str, model_name: str, api_key: str
662
+ ) -> dict:
663
+ """
664
+ Doucumentation:
665
+ https://api.charmhub.io/docs/model-service-admin.html#update_model
666
+ Endpoint: [PATCH]
667
+ https://api.charmhub.io/v1/brand/{store_id}/model/{model_name}
668
+ """
669
+ response = self.session.patch(
670
+ url=self.get_endpoint_url(f"brand/{store_id}/model/{model_name}"),
671
+ headers=self._get_dev_token_authorization_header(session),
672
+ json={"api-key": api_key},
673
+ )
674
+
675
+ return self.process_response(response)
676
+
677
+ def get_store_model_policies(
678
+ self, session: dict, store_id: str, model_name: str
679
+ ) -> dict:
680
+ """
681
+ Documentation:
682
+ https://api.charmhub.io/docs/model-service-admin.html#read_serial_policies
683
+ Endpoint: [GET]
684
+ https://api.charmhub.io/v1/brand/{store_id}/model/<model_name>/serial_policy
685
+ """
686
+ response = self.session.get(
687
+ url=self.get_endpoint_url(
688
+ f"brand/{store_id}/model/{model_name}/serial_policy"
689
+ ),
690
+ headers=self._get_dev_token_authorization_header(session),
691
+ )
692
+
693
+ return self.process_response(response)
694
+
695
+ def create_store_model_policy(
696
+ self,
697
+ session: dict,
698
+ store_id: str,
699
+ model_name: str,
700
+ signing_key: str,
701
+ ) -> dict:
702
+ """
703
+ Documentation:
704
+ https://api.charmhub.io/docs/model-service-admin.html#create_serial_policy
705
+ Endpoint: [POST]
706
+ https://api.charmhub.io/v1/brand/{store_id}/model/{model_name}/serial_policy
707
+ """
708
+ response = self.session.post(
709
+ url=self.get_endpoint_url(
710
+ f"brand/{store_id}/model/{model_name}/serial_policy"
711
+ ),
712
+ headers=self._get_dev_token_authorization_header(session),
713
+ json={"signing-key-sha3-384": signing_key},
714
+ )
715
+
716
+ return self.process_response(response)
717
+
718
+ def delete_store_model_policy(
719
+ self,
720
+ session: dict,
721
+ store_id: str,
722
+ model_name: str,
723
+ rev: str,
724
+ ) -> dict:
725
+ """
726
+ Documentation:
727
+ https://api.charmhub.io/docs/model-service-admin.html#delete_serial_policy
728
+ Endpoint: [DELETE]
729
+ https://api.charmhub.io/v1/brand/{store_id}/model/{model_name}/serial_policy/{serial_policy_revision}
730
+ """
731
+ url = self.get_endpoint_url(
732
+ f"brand/{store_id}/model/{model_name}/serial_policy/{rev}"
733
+ )
734
+ response = self.session.delete(
735
+ url=url,
736
+ headers=self._get_dev_token_authorization_header(session),
737
+ )
738
+
739
+ return response
740
+
741
+ def get_store_signing_keys(self, session: dict, store_id: str) -> dict:
742
+ """
743
+ Documentation:
744
+ https://api.charmhub.io/docs/model-service-admin.html#read_signing_keys
745
+ Endpoint: [GET] https://api.charmhub.io/v1/brand/{store_id}/signing_key
746
+ """
747
+ headers = self._get_dev_token_authorization_header(session)
748
+ response = self.session.get(
749
+ url=self.get_endpoint_url(f"brand/{store_id}/signing_key"),
750
+ headers=headers,
751
+ )
752
+ return self.process_response(response)
753
+
754
+ def create_store_signing_key(
755
+ self, session: dict, store_id: str, name: str
756
+ ) -> dict:
757
+ """
758
+ Documentation:
759
+ https://api.charmhub.io/docs/model-service-admin.html#create_signing_key
760
+ Endpoint: [POST]
761
+ https://api.charmhub.io/v1/brand/{store_id}/signing_key
762
+ """
763
+ headers = self._get_dev_token_authorization_header(session)
764
+ response = self.session.post(
765
+ url=self.get_endpoint_url(f"brand/{store_id}/signing_key"),
766
+ headers=headers,
767
+ json={"name": name},
768
+ )
769
+ return self.process_response(response)
770
+
771
+ def delete_store_signing_key(
772
+ self, session: dict, store_id: str, signing_key_sha3_384: str
773
+ ) -> dict:
774
+ """
775
+ Documentation:
776
+ https://api.charmhub.io/docs/model-service-admin.html#delete_signing_key
777
+ Endpoint: [DELETE]
778
+ https://api.charmhub.io/v1/brand/{store_id}/signing_key/<signing_key_sha3_384}
779
+ """
780
+ headers = self._get_dev_token_authorization_header(session)
781
+ url = self.get_endpoint_url(
782
+ f"brand/{store_id}/signing_key/{signing_key_sha3_384}"
783
+ )
784
+ response = self.session.delete(
785
+ url=url,
786
+ headers=headers,
787
+ )
788
+
789
+ return response
790
+
791
+ def get_brand(self, session: dict, store_id: str) -> dict:
792
+ """
793
+ Documentation:
794
+ https://api.charmhub.io/docs/model-service-admin.html#read_brand
795
+ Endpoint: [GET] https://api.charmhub.io/v1/brand/{store_id}
796
+ """
797
+ headers = self._get_dev_token_authorization_header(session)
798
+ url = self.get_endpoint_url(f"brand/{store_id}")
799
+ response = self.session.get(
800
+ url=url,
801
+ headers=headers,
802
+ )
803
+ return self.process_response(response)
804
+
805
+ # FEATURED SNAP AUTOMATION
806
+ def delete_featured_snaps(
807
+ self, publisher_auth: str, packages: str
808
+ ) -> dict:
809
+ """
810
+ Documentation: (link to spec)
811
+ https://docs.google.com/document/d/1UAybxuZyErh3ayqb4nzL3T4BbvMtnmKKEPu-ixcCj_8
812
+ Endpoint: [DELETE] https://api.charmhub.io/v1/snap/featured
813
+ """
814
+ headers = self._get_authorization_header(publisher_auth)
815
+ url = self.get_endpoint_url("snap/featured")
816
+ response = self.session.delete(url=url, headers=headers, json=packages)
817
+ return response
818
+
819
+ def update_featured_snaps(self, publisher_auth: str, snaps: list) -> dict:
820
+ """
821
+ Documentation: (link to spec)
822
+ https://docs.google.com/document/d/1UAybxuZyErh3ayqb4nzL3T4BbvMtnmKKEPu-ixcCj_8
823
+ Endpoint: [PUT] https://api.charmhub.io/v1/{name_space}/featured
824
+ """
825
+ headers = self._get_authorization_header(publisher_auth)
826
+ url = self.get_endpoint_url("snap/featured")
827
+ response = self.session.put(
828
+ url=url,
829
+ headers=headers,
830
+ json=snaps,
831
+ )
832
+ return response