zabel-elements 1.47.2__tar.gz → 1.50.0__tar.gz

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.
Files changed (48) hide show
  1. {zabel_elements-1.47.2/zabel_elements.egg-info → zabel_elements-1.50.0}/PKG-INFO +2 -2
  2. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/pyproject.toml +2 -2
  3. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/atlassian.py +258 -67
  4. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/confluence.py +107 -0
  5. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/github.py +0 -2
  6. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/githubcloud.py +540 -88
  7. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/jira.py +616 -37
  8. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/jiracloud.py +45 -0
  9. {zabel_elements-1.47.2 → zabel_elements-1.50.0/zabel_elements.egg-info}/PKG-INFO +2 -2
  10. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/LICENSE.txt +0 -0
  11. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/README.md +0 -0
  12. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/setup.cfg +0 -0
  13. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/__init__.py +0 -0
  14. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/actito.py +0 -0
  15. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/artifactory.py +0 -0
  16. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/atlassian.py +0 -0
  17. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/__init__.py +0 -0
  18. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/actito.py +0 -0
  19. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/artifactory.py +0 -0
  20. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/confluencecloud.py +0 -0
  21. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/gitlab.py +0 -0
  22. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/jenkins.py +0 -0
  23. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/kubernetes.py +0 -0
  24. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/okta.py +0 -0
  25. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/sonarqube.py +0 -0
  26. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/sonatypenexus.py +0 -0
  27. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/base/squashtm.py +0 -0
  28. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/confluence.py +0 -0
  29. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/confluencecloud.py +0 -0
  30. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/github.py +0 -0
  31. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/githubcloud.py +0 -0
  32. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/gitlab.py +0 -0
  33. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/jenkins.py +0 -0
  34. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/jira.py +0 -0
  35. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/jiracloud.py +0 -0
  36. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/kubehelpers.py +0 -0
  37. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/kubernetes.py +0 -0
  38. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/okta.py +0 -0
  39. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/sonarqube.py +0 -0
  40. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/sonatypenexus.py +0 -0
  41. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/clients/squashtm.py +0 -0
  42. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/images/__init__.py +0 -0
  43. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/images/managedserviceapp.py +0 -0
  44. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel/elements/images/utilityapp.py +0 -0
  45. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel_elements.egg-info/SOURCES.txt +0 -0
  46. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel_elements.egg-info/dependency_links.txt +0 -0
  47. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel_elements.egg-info/requires.txt +0 -0
  48. {zabel_elements-1.47.2 → zabel_elements-1.50.0}/zabel_elements.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zabel-elements
3
- Version: 1.47.2
4
- Summary: TThe Zabel default clients and images
3
+ Version: 1.50.0
4
+ Summary: The Zabel default clients and images
5
5
  Author-email: Martin Lafaix <martin.lafaix@external.engie.com>
6
6
  License-Expression: EPL-2.0
7
7
  Project-URL: Homepage, https://github.com/engie-group/zabel-elements
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zabel-elements"
7
- version = "1.47.2"
8
- description = "TThe Zabel default clients and images"
7
+ version = "1.50.0"
8
+ description = "The Zabel default clients and images"
9
9
  readme = "README.md"
10
10
  license = "EPL-2.0"
11
11
  authors = [
@@ -11,7 +11,7 @@ pagination if appropriate, but does not process the results or compose
11
11
  API requests.
12
12
  """
13
13
 
14
- from typing import Any, Iterable, Mapping
14
+ from typing import Any, Iterable, Mapping, Optional
15
15
 
16
16
  import random
17
17
  import time
@@ -87,11 +87,12 @@ class Atlassian:
87
87
  ```python
88
88
  from zabel.elements.clients import Atlassian
89
89
 
90
- url = 'https://api.atlassian.com/admin/v1/'
90
+ url = 'https://api.atlassian.com'
91
91
  token = '...'
92
92
  atlassian = Atlassian(url, bearer_auth=token)
93
93
  atlassian.list_organization_users('your-organization-id')
94
94
  ```
95
+
95
96
  """
96
97
 
97
98
  def __init__(
@@ -121,21 +122,28 @@ class Atlassian:
121
122
 
122
123
  `url` is the top-level API endpoint. For example:
123
124
 
124
- 'https://api.atlassian.com/admin/v1/'
125
+ 'https://api.atlassian.com'
125
126
  """
126
127
  ensure_nonemptystring('url')
127
128
  ensure_noneornonemptystring('bearer_auth')
128
129
  ensure_noneorinstance('basic_auth', tuple)
129
- if not url.endswith('/v1/'):
130
- raise ValueError(f'url must end with /v1/: {url}')
131
130
 
132
131
  if basic_auth is None and bearer_auth is None:
133
132
  raise ValueError(
134
133
  'Provide at least one of bearer_auth or basic_auth'
135
134
  )
136
135
 
137
- self.url = url
138
- self.base_url = url.removesuffix('/v1/')
136
+ # Normalize url: strip legacy /admin/v1 or /admin suffix so
137
+ # both old inputs (e.g. 'https://api.atlassian.com/admin/v1/')
138
+ # and new inputs (e.g. 'https://api.atlassian.com') work.
139
+ normalized = url.rstrip('/')
140
+ for suffix in ('/admin/v1', '/admin'):
141
+ if normalized.endswith(suffix):
142
+ normalized = normalized[: -len(suffix)]
143
+ break
144
+ self.url = normalized
145
+ self.admin_url = join_url(self.url, 'admin')
146
+ self.v1_url = join_url(self.url, 'admin/v1')
139
147
  self.basic_auth = basic_auth
140
148
  self.bearer_auth = bearer_auth
141
149
 
@@ -183,21 +191,21 @@ class Atlassian:
183
191
  A list of _users_. Each user is a dictionary with the
184
192
  following entries:
185
193
 
186
- - `account_id`: a string
187
- - `account_type`: a string
188
- - `account_status`: a string
189
- - `name`: a string
190
- - `picture`: a string
191
- - `email`: a string
192
- - `access_billable`: a boolean
193
- - `last_active`: a string
194
- - `product_access`: a list of dictionaries
195
- - `links`: a dictionary (with `self`)
194
+ - account_id: a string
195
+ - account_type: a string
196
+ - account_status: a string
197
+ - name: a string
198
+ - picture: a string
199
+ - email: a string
200
+ - access_billable: a boolean
201
+ - last_active: a string
202
+ - product_access: a list of dictionaries
203
+ - links: a dictionary (with self)
196
204
  """
197
205
  ensure_nonemptystring('org_id')
198
206
 
199
207
  session = self._get_session()
200
- api_url = join_url(self.url, f'orgs/{org_id}/users')
208
+ api_url = join_url(self.v1_url, f'orgs/{org_id}/users')
201
209
 
202
210
  collected: list[dict[str, Any]] = []
203
211
  while True:
@@ -235,7 +243,7 @@ class Atlassian:
235
243
  # Returned value
236
244
 
237
245
  A list of _directories_. Each directory is a dictionary with
238
- entries such as `id`, `name`, `type`, `managementAccess`.
246
+ entries such as id, name, type, managementAccess.
239
247
  """
240
248
  ensure_nonemptystring('org_id')
241
249
 
@@ -255,8 +263,8 @@ class Atlassian:
255
263
  # Returned value
256
264
 
257
265
  A list of _groups_. Each group is a dictionary with entries
258
- such as `id`, `name`, `description`, `directoryId`,
259
- `managementAccess`, `externalSynced`.
266
+ such as id, name, description, directoryId,
267
+ managementAccess, externalSynced.
260
268
  """
261
269
  ensure_nonemptystring('org_id')
262
270
  ensure_nonemptystring('directory_id')
@@ -287,11 +295,11 @@ class Atlassian:
287
295
  A list of _role assignments_. Each role assignment is a
288
296
  dictionary with:
289
297
 
290
- - `resourceId`: a string (e.g., `ari:cloud:confluence::site/<id>`)
291
- - `resourceOwner`: a string (`jira`, `confluence`, `cmdb`, ...)
292
- - `roles`: a list of strings (`atlassian/user`, `atlassian/admin`,
293
- `atlassian/guest`)
294
- - `defaultRole`: a string
298
+ - resourceId: a string (e.g., ari:cloud:confluence::site/<id>)
299
+ - resourceOwner: a string (jira, confluence, cmdb, ...)
300
+ - roles: a list of strings (atlassian/user, atlassian/admin,
301
+ atlassian/guest)
302
+ - defaultRole: a string
295
303
  """
296
304
  ensure_nonemptystring('org_id')
297
305
  ensure_nonemptystring('directory_id')
@@ -311,11 +319,11 @@ class Atlassian:
311
319
  ) -> list[dict[str, Any]]:
312
320
  """List users in a directory of an organization.
313
321
 
314
- Returns full user info including the org-level `status` field
315
- (`active` / `suspended` / `deactivated` / `not_invited` /
316
- `for_deletion`), which is the source of truth for billing.
317
- Note the difference with `accountStatus` returned by site-level
318
- APIs: the latter can report `active` for users actually
322
+ Returns full user info including the org-level status field
323
+ (active / suspended / deactivated / not_invited /
324
+ for_deletion), which is the source of truth for billing.
325
+ Note the difference with accountStatus returned by site-level
326
+ APIs: the latter can report active for users actually
319
327
  suspended at the org level.
320
328
 
321
329
  # Required parameters
@@ -333,14 +341,14 @@ class Atlassian:
333
341
  A list of _users_. Each user is a dictionary with entries
334
342
  such as:
335
343
 
336
- - `accountId`: a string
337
- - `accountType`: a string (`atlassian`, `app`, `customer`)
338
- - `status`: a string (`active`, `suspended`, `deactivated`)
339
- - `accountStatus`: a string (often `active` even when status is
340
- suspended -- prefer `status` for billing decisions)
341
- - `membershipStatus`: a string (per-directory membership)
342
- - `name`, `nickname`, `email`, `claimStatus`, `addedToOrg`,
343
- `deactivatedOn`, ...
344
+ - accountId: a string
345
+ - accountType: a string (atlassian, app, customer)
346
+ - status: a string (active, suspended, deactivated)
347
+ - accountStatus: a string (often active even when status is
348
+ suspended -- prefer status for billing decisions)
349
+ - membershipStatus: a string (per-directory membership)
350
+ - name, nickname, email, claimStatus, addedToOrg,
351
+ deactivatedOn, ...
344
352
 
345
353
  # Usage
346
354
 
@@ -354,7 +362,8 @@ class Atlassian:
354
362
  users = atlassian.list_directory_users(org_id, directory_id, params)
355
363
  ```
356
364
 
357
- For billing, combining `roleIds` + `resourceIds` returns the
365
+
366
+ For billing, combining roleIds + resourceIds returns the
358
367
  exact billable user set for a given (site, product) couple --
359
368
  matching admin UI counts.
360
369
  """
@@ -375,20 +384,20 @@ class Atlassian:
375
384
 
376
385
  # Required parameters
377
386
 
378
- - site_url: a non-empty string (of the form `https://...`)
387
+ - site_url: a non-empty string (of the form https://...)
379
388
 
380
389
  # Returned value
381
390
 
382
391
  A list of _users_. Each user is a dictionary with the
383
392
  following entries:
384
393
 
385
- - `accountId`: a string
386
- - `accountType`: a string
387
- - `emailAddress`: a string
388
- - `avatarUrls`: a dictionary
389
- - `displayName`: a string
390
- - `active`: a boolean
391
- - `locale`: a string
394
+ - accountId: a string
395
+ - accountType: a string
396
+ - emailAddress: a string
397
+ - avatarUrls: a dictionary
398
+ - displayName: a string
399
+ - active: a boolean
400
+ - locale: a string
392
401
  """
393
402
  ensure_nonemptystring('site_url')
394
403
 
@@ -452,8 +461,8 @@ class Atlassian:
452
461
  """List Jira application roles on a site.
453
462
 
454
463
  Each role describes one Jira product available on the site
455
- (e.g. `jira-software`, `jira-servicedesk`, `jira-core`,
456
- `jira-product-discovery`) and exposes the groups whose members
464
+ (e.g. jira-software, jira-servicedesk, jira-core,
465
+ jira-product-discovery) and exposes the groups whose members
457
466
  are granted access to that product.
458
467
 
459
468
  # Required parameters
@@ -465,14 +474,14 @@ class Atlassian:
465
474
  A list of _roles_. Each role is a dictionary with entries
466
475
  such as:
467
476
 
468
- - `key`: a string (the product key)
469
- - `name`: a string
470
- - `groups`: a list of strings (group names)
471
- - `groupDetails`: a list of dictionaries (with `name`, `groupId`)
472
- - `defaultGroups`: a list of strings
473
- - `numberOfSeats`: an integer
474
- - `userCount`: an integer
475
- - `hasUnlimitedSeats`: a boolean
477
+ - key: a string (the product key)
478
+ - name: a string
479
+ - groups: a list of strings (group names)
480
+ - groupDetails: a list of dictionaries (with name, groupId)
481
+ - defaultGroups: a list of strings
482
+ - numberOfSeats: an integer
483
+ - userCount: an integer
484
+ - hasUnlimitedSeats: a boolean
476
485
  """
477
486
  ensure_nonemptystring('site_url')
478
487
 
@@ -492,7 +501,7 @@ class Atlassian:
492
501
  """List members of a group on a Jira site.
493
502
 
494
503
  Works for any group on the site, including the groups that
495
- gate Confluence access (e.g. `confluence-users`).
504
+ gate Confluence access (e.g. confluence-users).
496
505
 
497
506
  # Required parameters
498
507
 
@@ -507,11 +516,11 @@ class Atlassian:
507
516
 
508
517
  A list of _users_. Each user is a dictionary with entries:
509
518
 
510
- - `accountId`: a string
511
- - `accountType`: a string
512
- - `displayName`: a string
513
- - `active`: a boolean
514
- - `emailAddress`: a string (omitted when the caller does not
519
+ - accountId: a string
520
+ - accountType: a string
521
+ - displayName: a string
522
+ - active: a boolean
523
+ - emailAddress: a string (omitted when the caller does not
515
524
  have the relevant privacy permission)
516
525
  """
517
526
  ensure_nonemptystring('site_url')
@@ -538,9 +547,181 @@ class Atlassian:
538
547
 
539
548
  return collected
540
549
 
550
+ ####################################################################
551
+ # Jira Rules
552
+ #
553
+ # list_rules
554
+ # get_rule
555
+ # enable_rule
556
+ # disable_rule
557
+
558
+ @api_call
559
+ @require_basic_auth
560
+ def list_rules(
561
+ self,
562
+ product: str,
563
+ site_id: str,
564
+ ) -> list[dict[str, Any]]:
565
+ """List rules for a Jira product on a site.
566
+ # Required parameters
567
+
568
+ - product: a non-empty string (e.g. jira, confluence)
569
+ - site_id: a non-empty string (the site ID, not the URL)
570
+
571
+ # Returned value
572
+ A list of _rules_. Each rule is a dictionary with entries such as:
573
+ - uuid: a string
574
+ - name: a string
575
+ - state: a string (ENABLED, DISABLED)
576
+ - description: a string
577
+ - authorAccountId: a string
578
+ - actorAccountId: a string
579
+ - created: a float (Unix timestamp)
580
+ - updated: a float (Unix timestamp)
581
+ - labels: a list of strings
582
+ - ruleScopeARIs: a list of strings
583
+ """
584
+ ensure_nonemptystring('product')
585
+ ensure_nonemptystring('site_id')
586
+
587
+ session = self._get_session()
588
+ next_cursor = ''
589
+ api = join_url(
590
+ self.url,
591
+ f'automation/public/{product}/{site_id}/rest/v1/rule/summary',
592
+ )
593
+ collected: list[dict[str, Any]] = []
594
+ while True:
595
+ url = api if not next_cursor else join_url(api, next_cursor)
596
+ response = session.get(url)
597
+ data = response.json()
598
+ values = data.get('data', [])
599
+ collected.extend(values)
600
+ links = data.get('links', {})
601
+ next_link = links.get('next')
602
+ if not values or not next_link:
603
+ break
604
+ next_cursor = next_link
605
+ return collected
606
+
607
+ @api_call
608
+ @require_basic_auth
609
+ def get_rule(
610
+ self,
611
+ product: str,
612
+ site_id: str,
613
+ rule_id: str,
614
+ ) -> dict[str, Any]:
615
+ """Get a rule for a Jira product on a site.
616
+ # Required parameters
617
+
618
+ - product: a non-empty string (e.g. jira, confluence)
619
+ - site_id: a non-empty string (the site ID, not the URL)
620
+ - rule_id: a non-empty string
621
+
622
+ # Returned value
623
+
624
+ A dictionary with two top-level keys:
625
+
626
+ - rule: a dictionary with entries such as:
627
+ - uuid: a string
628
+ - name: a string
629
+ - state: a string (ENABLED or DISABLED)
630
+ - description: a string or None
631
+ - canOtherRuleTrigger: a boolean
632
+ - notifyOnError: a string (e.g. FIRSTERROR)
633
+ - authorAccountId: a string
634
+ - actor: a dictionary with keys type and actor (account ID)
635
+ - trigger: a dictionary describing the trigger component
636
+ - components: a list of dictionaries (conditions and actions)
637
+ - ruleScopeARIs: a list of strings (project ARIs)
638
+ - labels: a list of strings
639
+ - writeAccessType: a string (e.g. UNRESTRICTED)
640
+ - collaborators: a list
641
+ - created: a float (Unix timestamp)
642
+ - updated: a float (Unix timestamp)
643
+
644
+ - connections: a list of _connections_. Each connection is a
645
+ dictionary with entries such as:
646
+ - id: a string
647
+ - accountId: a string
648
+ - connectionTargetKey: a string
649
+ - targetConfigJson: a string
650
+ - authType: a string
651
+ - createdAt: a float (Unix timestamp)
652
+ - updatedAt: a float (Unix timestamp)
653
+ - container: a dictionary
654
+ """
655
+ ensure_nonemptystring('product')
656
+ ensure_nonemptystring('site_id')
657
+ ensure_nonemptystring('rule_id')
658
+
659
+ session = self._get_session()
660
+ api = join_url(
661
+ self.url,
662
+ f'automation/public/{product}/{site_id}/rest/v1/rule/{rule_id}',
663
+ )
664
+ response = session.get(api)
665
+ return response.json()
666
+
667
+ @api_call
668
+ @require_basic_auth
669
+ def enable_rule(
670
+ self,
671
+ product: str,
672
+ site_id: str,
673
+ rule_id: str,
674
+ ):
675
+ """Enable a rule for a Jira product on a site.
676
+ # Required parameters
677
+
678
+ - product: a non-empty string (e.g. jira, confluence)
679
+ - site_id: a non-empty string (the site ID, not the URL)
680
+ - rule_id: a non-empty string
681
+ """
682
+ ensure_nonemptystring('product')
683
+ ensure_nonemptystring('site_id')
684
+ ensure_nonemptystring('rule_id')
685
+
686
+ self._set_rule_state(product, site_id, rule_id, 'ENABLED')
687
+
688
+ @api_call
689
+ @require_basic_auth
690
+ def disable_rule(
691
+ self,
692
+ product: str,
693
+ site_id: str,
694
+ rule_id: str,
695
+ ):
696
+ """Disable a rule for a Jira product on a site.
697
+
698
+ # Required parameters
699
+
700
+ - product: a non-empty string (e.g. jira, confluence)
701
+ - site_id: a non-empty string (the site ID, not the URL)
702
+ - rule_id: a non-empty string
703
+ """
704
+ ensure_nonemptystring('product')
705
+ ensure_nonemptystring('site_id')
706
+ ensure_nonemptystring('rule_id')
707
+
708
+ self._set_rule_state(product, site_id, rule_id, 'DISABLED')
709
+
541
710
  ####################################################################
542
711
  # atlassian private helpers
543
712
 
713
+ def _set_rule_state(
714
+ self,
715
+ product: str,
716
+ site_id: str,
717
+ rule_id: str,
718
+ state: str,
719
+ ) -> None:
720
+ self._put(
721
+ f'automation/public/{product}/{site_id}/rest/v1/rule/{rule_id}/state',
722
+ json={'value': state},
723
+ )
724
+
544
725
  def _get(
545
726
  self,
546
727
  api: str,
@@ -551,6 +732,16 @@ class Atlassian:
551
732
  api_url = join_url(self.url, api)
552
733
  return session.get(api_url, params=params)
553
734
 
735
+ def _put(
736
+ self, api: str, json: Optional[Mapping[str, Any]] = None
737
+ ) -> requests.Response:
738
+ session = self._get_session()
739
+ api_url = join_url(self.url, api)
740
+ return session.put(
741
+ api_url,
742
+ json=json,
743
+ )
744
+
554
745
  def _collect_v2(
555
746
  self,
556
747
  api: str,
@@ -558,13 +749,13 @@ class Atlassian:
558
749
  ) -> list[dict[str, Any]]:
559
750
  """Return admin/v2 GET api call results, collected.
560
751
 
561
- First page sends `params` (server-side filters); subsequent
752
+ First page sends params (server-side filters); subsequent
562
753
  pages send the cursor alone -- it encodes the filters, and
563
754
  mixing them again causes inconsistent results on some
564
- endpoints (e.g. directory `users/`). Retries on HTTP 429.
755
+ endpoints (e.g. directory users/). Retries on HTTP 429.
565
756
  """
566
757
  session = self._get_session()
567
- api_url = join_url(join_url(self.base_url, '/v2'), api)
758
+ api_url = join_url(join_url(self.admin_url, '/v2'), api)
568
759
 
569
760
  _params: dict[str, Any] = {
570
761
  'limit': _V2_PAGE_SIZE,
@@ -249,6 +249,77 @@ class Confluence:
249
249
 
250
250
  return self._collect_data('content/search', params=params)
251
251
 
252
+ @api_call
253
+ def audit(
254
+ self,
255
+ group: str,
256
+ actions: Optional[str] = None,
257
+ from_date: Optional[str] = None,
258
+ to_date: Optional[str] = None,
259
+ limit: int = 100,
260
+ scan_limit: Optional[int] = None,
261
+ ) -> List[Dict[str, Any]]:
262
+ """Return all audit log events matching a group filter.
263
+
264
+ # Required parameters
265
+
266
+ - group: a non-empty string
267
+
268
+ # Optional parameters
269
+
270
+ - actions: a string or None (None by default)
271
+ - from_date: a string or None (None by default)
272
+ - to_date: a string or None (None by default)
273
+ - limit: an integer (`100` by default)
274
+ - scan_limit: an integer or None (None by default)
275
+
276
+ # Returned value
277
+
278
+ A possibly empty list of audit log event dictionaries.
279
+
280
+ Handles pagination (i.e., it returns all matching events, not
281
+ only the first n events).
282
+
283
+ # Raised exceptions
284
+
285
+ If the request fails, an _ApiError_ is raised.
286
+ """
287
+ ensure_nonemptystring('group')
288
+ ensure_noneorinstance('actions', str)
289
+ ensure_noneorinstance('from_date', str)
290
+ ensure_noneorinstance('to_date', str)
291
+ ensure_instance('limit', int)
292
+ ensure_noneorinstance('scan_limit', int)
293
+
294
+ params: Dict[str, Any] = {
295
+ 'search': group,
296
+ 'limit': limit,
297
+ }
298
+ add_if_specified(params, 'actions', actions)
299
+ add_if_specified(params, 'scanLimit', scan_limit)
300
+ add_if_specified(params, 'from', from_date)
301
+ add_if_specified(params, 'to', to_date)
302
+
303
+ all_events: List[Dict[str, Any]] = []
304
+ while True:
305
+ response = self.session().get(
306
+ join_url(self.url, '/rest/auditing/1.0/events'),
307
+ params=params,
308
+ )
309
+ if response.status_code // 100 != 2:
310
+ raise ApiError(response.text)
311
+ data = response.json()
312
+ entities = data.get('entities', [])
313
+ all_events.extend(entities)
314
+ paging = data.get('pagingInfo', {})
315
+ if paging.get('lastPage', True) or not entities:
316
+ break
317
+ cursor = paging.get('nextPageCursor')
318
+ if not cursor:
319
+ break
320
+ params['pageCursor'] = cursor
321
+ return all_events
322
+
252
323
  ####################################################################
253
324
  # Confluence groups&users
254
325
  #
@@ -957,6 +1028,8 @@ class Confluence:
957
1028
  # list_space_permissionsets*
958
1029
  # create_space
959
1030
  # delete_space
1031
+ # archive_space
1032
+ # restore_space
960
1033
  # add_space_label*
961
1034
  # remove_space_permission*
962
1035
  # add_space_permissions*
@@ -1262,6 +1335,40 @@ class Confluence:
1262
1335
  response = self._delete(f'/space/{space_key}')
1263
1336
  return response
1264
1337
 
1338
+ @api_call
1339
+ def archive_space(self, space_key: str) -> bool:
1340
+ """Archive a space.
1341
+
1342
+ # Required parameters
1343
+
1344
+ - space_key: a non-empty string
1345
+
1346
+ # Returned value
1347
+
1348
+ A boolean, True if successful
1349
+ """
1350
+ ensure_nonemptystring('space_key')
1351
+
1352
+ response = self._put(f'/space/{space_key}/archive')
1353
+ return response.status_code == 204
1354
+
1355
+ @api_call
1356
+ def restore_space(self, space_key: str) -> bool:
1357
+ """Restore a space.
1358
+
1359
+ # Required parameters
1360
+
1361
+ - space_key: a non-empty string
1362
+
1363
+ # Returned value
1364
+
1365
+ A boolean, True if successful
1366
+ """
1367
+ ensure_nonemptystring('space_key')
1368
+
1369
+ response = self._put(f'/space/{space_key}/restore')
1370
+ return response.status_code == 204
1371
+
1265
1372
  @api_call
1266
1373
  def add_space_label(self, space_key: str, label: str) -> bool:
1267
1374
  """Add label to space.
@@ -4582,12 +4582,10 @@ class GitHub:
4582
4582
 
4583
4583
  remaining = response.headers.get('X-RateLimit-Remaining')
4584
4584
  retry_after = response.headers.get('Retry-After')
4585
- reset = response.headers.get('X-RateLimit-Reset')
4586
4585
  if (
4587
4586
  response.status_code == 403
4588
4587
  and remaining != '0'
4589
4588
  and retry_after is None
4590
- and reset is None
4591
4589
  ):
4592
4590
  return response
4593
4591