dockerhub-api 0.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.
- dockerhub_api/__init__.py +80 -0
- dockerhub_api/__main__.py +4 -0
- dockerhub_api/agent_server.py +92 -0
- dockerhub_api/api/__init__.py +1 -0
- dockerhub_api/api/api_client_access_tokens.py +77 -0
- dockerhub_api/api/api_client_audit_logs.py +56 -0
- dockerhub_api/api/api_client_auth.py +80 -0
- dockerhub_api/api/api_client_base.py +338 -0
- dockerhub_api/api/api_client_groups.py +158 -0
- dockerhub_api/api/api_client_org_access_tokens.py +106 -0
- dockerhub_api/api/api_client_orgs.py +149 -0
- dockerhub_api/api/api_client_repositories.py +217 -0
- dockerhub_api/api/api_client_scim.py +153 -0
- dockerhub_api/api_client.py +35 -0
- dockerhub_api/auth.py +252 -0
- dockerhub_api/dockerhub_input_models.py +756 -0
- dockerhub_api/dockerhub_response_models.py +344 -0
- dockerhub_api/main_agent.json +14 -0
- dockerhub_api/mcp/__init__.py +120 -0
- dockerhub_api/mcp/mcp_admin.py +45 -0
- dockerhub_api/mcp/mcp_audit.py +44 -0
- dockerhub_api/mcp/mcp_auth.py +66 -0
- dockerhub_api/mcp/mcp_org.py +58 -0
- dockerhub_api/mcp/mcp_repos.py +58 -0
- dockerhub_api/mcp/mcp_scim.py +56 -0
- dockerhub_api/mcp/mcp_teams.py +55 -0
- dockerhub_api/mcp_config.json +3 -0
- dockerhub_api/mcp_server.py +109 -0
- dockerhub_api-0.1.0.dist-info/METADATA +230 -0
- dockerhub_api-0.1.0.dist-info/RECORD +34 -0
- dockerhub_api-0.1.0.dist-info/WHEEL +5 -0
- dockerhub_api-0.1.0.dist-info/entry_points.txt +3 -0
- dockerhub_api-0.1.0.dist-info/licenses/LICENSE +21 -0
- dockerhub_api-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Organization endpoints: settings, members, and invites.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.0 — core wrapper.
|
|
4
|
+
CONCEPT:HUB-1.3 — destructive-action gating (member removal, invite deletion,
|
|
5
|
+
org-settings writes).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
11
|
+
from dockerhub_api.dockerhub_input_models import (
|
|
12
|
+
BulkInviteModel,
|
|
13
|
+
InviteModel,
|
|
14
|
+
OrgMemberListModel,
|
|
15
|
+
OrgMemberModel,
|
|
16
|
+
OrgMemberUpdateModel,
|
|
17
|
+
OrgSettingsModel,
|
|
18
|
+
)
|
|
19
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
20
|
+
BulkInviteResult,
|
|
21
|
+
InvitePage,
|
|
22
|
+
OrgMember,
|
|
23
|
+
OrgMemberPage,
|
|
24
|
+
OrgSettings,
|
|
25
|
+
validate_lenient,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DockerHubApiOrgs(DockerHubApiBase):
|
|
30
|
+
"""``/v2/orgs/{org}/settings``, ``/members``, and ``/v2/invites``."""
|
|
31
|
+
|
|
32
|
+
# ----------------------------- settings ----------------------------- #
|
|
33
|
+
|
|
34
|
+
def get_org_settings(self, org: str) -> dict[str, Any]:
|
|
35
|
+
"""Get an organization's settings (restricted images policy)."""
|
|
36
|
+
envelope = self._request("GET", f"/v2/orgs/{org}/settings")
|
|
37
|
+
envelope["data"] = validate_lenient(OrgSettings, envelope["data"])
|
|
38
|
+
return envelope
|
|
39
|
+
|
|
40
|
+
def update_org_settings(
|
|
41
|
+
self,
|
|
42
|
+
org: str,
|
|
43
|
+
restricted_images_enabled: bool,
|
|
44
|
+
allow_official_images: bool = True,
|
|
45
|
+
allow_verified_publishers: bool = True,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Replace an organization's settings. Destructive — gated.
|
|
48
|
+
|
|
49
|
+
Controls ``restricted_images``: whether members may only pull
|
|
50
|
+
org-approved images, with carve-outs for Docker Official Images and
|
|
51
|
+
Verified Publisher images.
|
|
52
|
+
"""
|
|
53
|
+
self._guard_destructive("update_org_settings")
|
|
54
|
+
model = OrgSettingsModel(
|
|
55
|
+
org=org,
|
|
56
|
+
restricted_images_enabled=restricted_images_enabled,
|
|
57
|
+
allow_official_images=allow_official_images,
|
|
58
|
+
allow_verified_publishers=allow_verified_publishers,
|
|
59
|
+
)
|
|
60
|
+
envelope = self._request(
|
|
61
|
+
"PUT", f"/v2/orgs/{model.org}/settings", json=model.payload
|
|
62
|
+
)
|
|
63
|
+
envelope["data"] = validate_lenient(OrgSettings, envelope["data"])
|
|
64
|
+
return envelope
|
|
65
|
+
|
|
66
|
+
# ----------------------------- members ------------------------------ #
|
|
67
|
+
|
|
68
|
+
def get_org_members(
|
|
69
|
+
self,
|
|
70
|
+
org: str,
|
|
71
|
+
search: str | None = None,
|
|
72
|
+
member_type: str | None = None,
|
|
73
|
+
role: str | None = None,
|
|
74
|
+
page: int | None = None,
|
|
75
|
+
page_size: int | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""List organization members (filter by search/type/role; paginated)."""
|
|
78
|
+
model = OrgMemberListModel(
|
|
79
|
+
org=org,
|
|
80
|
+
search=search,
|
|
81
|
+
member_type=member_type,
|
|
82
|
+
role=role,
|
|
83
|
+
page=page,
|
|
84
|
+
page_size=page_size,
|
|
85
|
+
)
|
|
86
|
+
envelope = self._request(
|
|
87
|
+
"GET", f"/v2/orgs/{model.org}/members", params=model.api_parameters
|
|
88
|
+
)
|
|
89
|
+
envelope["data"] = validate_lenient(OrgMemberPage, envelope["data"])
|
|
90
|
+
return envelope
|
|
91
|
+
|
|
92
|
+
def export_org_members(self, org: str) -> dict[str, Any]:
|
|
93
|
+
"""Export the member list as CSV (``GET /members/export``)."""
|
|
94
|
+
return self._request("GET", f"/v2/orgs/{org}/members/export", accept="text/csv")
|
|
95
|
+
|
|
96
|
+
def update_org_member(self, org: str, username: str, role: str) -> dict[str, Any]:
|
|
97
|
+
"""Set a member's org role (``owner``, ``editor``, or ``member``)."""
|
|
98
|
+
model = OrgMemberUpdateModel(org=org, username=username, role=role)
|
|
99
|
+
envelope = self._request(
|
|
100
|
+
"PUT",
|
|
101
|
+
f"/v2/orgs/{model.org}/members/{model.username}",
|
|
102
|
+
json=model.payload,
|
|
103
|
+
)
|
|
104
|
+
envelope["data"] = validate_lenient(OrgMember, envelope["data"])
|
|
105
|
+
return envelope
|
|
106
|
+
|
|
107
|
+
def remove_org_member(self, org: str, username: str) -> dict[str, Any]:
|
|
108
|
+
"""Remove a member from the organization. Destructive — gated."""
|
|
109
|
+
self._guard_destructive("remove_org_member")
|
|
110
|
+
model = OrgMemberModel(org=org, username=username)
|
|
111
|
+
return self._request("DELETE", f"/v2/orgs/{model.org}/members/{model.username}")
|
|
112
|
+
|
|
113
|
+
# ----------------------------- invites ------------------------------ #
|
|
114
|
+
|
|
115
|
+
def get_org_invites(self, org: str) -> dict[str, Any]:
|
|
116
|
+
"""List an organization's pending invites."""
|
|
117
|
+
envelope = self._request("GET", f"/v2/orgs/{org}/invites")
|
|
118
|
+
envelope["data"] = validate_lenient(InvitePage, envelope["data"])
|
|
119
|
+
return envelope
|
|
120
|
+
|
|
121
|
+
def delete_invite(self, invite_id: str) -> dict[str, Any]:
|
|
122
|
+
"""Cancel an invite. Destructive — gated."""
|
|
123
|
+
self._guard_destructive("delete_invite")
|
|
124
|
+
model = InviteModel(invite_id=invite_id)
|
|
125
|
+
return self._request("DELETE", f"/v2/invites/{model.invite_id}")
|
|
126
|
+
|
|
127
|
+
def resend_invite(self, invite_id: str) -> dict[str, Any]:
|
|
128
|
+
"""Resend an invite (``PATCH /v2/invites/{id}/resend``)."""
|
|
129
|
+
model = InviteModel(invite_id=invite_id)
|
|
130
|
+
return self._request("PATCH", f"/v2/invites/{model.invite_id}/resend")
|
|
131
|
+
|
|
132
|
+
def bulk_invite(
|
|
133
|
+
self,
|
|
134
|
+
org: str,
|
|
135
|
+
invitees: list[str],
|
|
136
|
+
team: str | None = None,
|
|
137
|
+
role: str = "member",
|
|
138
|
+
dry_run: bool = False,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Invite many users/emails at once (``POST /v2/invites/bulk``).
|
|
141
|
+
|
|
142
|
+
``dry_run=True`` validates the invitees without sending invites.
|
|
143
|
+
"""
|
|
144
|
+
model = BulkInviteModel(
|
|
145
|
+
org=org, invitees=invitees, team=team, role=role, dry_run=dry_run
|
|
146
|
+
)
|
|
147
|
+
envelope = self._request("POST", "/v2/invites/bulk", json=model.payload)
|
|
148
|
+
envelope["data"] = validate_lenient(BulkInviteResult, envelope["data"])
|
|
149
|
+
return envelope
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Repository and tag endpoints (``/v2/namespaces/{ns}/repositories``).
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.0 — core wrapper. Repository creation is the primary
|
|
4
|
+
provisioning use case (creating image repos for releases) and is therefore
|
|
5
|
+
*not* destructive-gated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
11
|
+
from dockerhub_api.dockerhub_input_models import (
|
|
12
|
+
ImmutableTagsPatchModel,
|
|
13
|
+
ImmutableTagsVerifyModel,
|
|
14
|
+
RepositoryCreateModel,
|
|
15
|
+
RepositoryGroupModel,
|
|
16
|
+
RepositoryListModel,
|
|
17
|
+
RepositoryModel,
|
|
18
|
+
TagListModel,
|
|
19
|
+
TagModel,
|
|
20
|
+
)
|
|
21
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
22
|
+
ImmutableTagsSettings,
|
|
23
|
+
ImmutableTagsVerification,
|
|
24
|
+
Repository,
|
|
25
|
+
RepositoryGroup,
|
|
26
|
+
RepositoryPage,
|
|
27
|
+
Tag,
|
|
28
|
+
TagPage,
|
|
29
|
+
validate_lenient,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DockerHubApiRepositories(DockerHubApiBase):
|
|
34
|
+
"""Repositories, tags, immutable tags, and repo-team permissions."""
|
|
35
|
+
|
|
36
|
+
def get_repositories(
|
|
37
|
+
self,
|
|
38
|
+
namespace: str,
|
|
39
|
+
name: str | None = None,
|
|
40
|
+
ordering: str | None = None,
|
|
41
|
+
page: int | None = None,
|
|
42
|
+
page_size: int | None = None,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""List a namespace's repositories (name filter + ordering enum)."""
|
|
45
|
+
model = RepositoryListModel(
|
|
46
|
+
namespace=namespace,
|
|
47
|
+
name=name,
|
|
48
|
+
ordering=ordering,
|
|
49
|
+
page=page,
|
|
50
|
+
page_size=page_size,
|
|
51
|
+
)
|
|
52
|
+
envelope = self._request(
|
|
53
|
+
"GET",
|
|
54
|
+
f"/v2/namespaces/{model.namespace}/repositories",
|
|
55
|
+
params=model.api_parameters,
|
|
56
|
+
)
|
|
57
|
+
envelope["data"] = validate_lenient(RepositoryPage, envelope["data"])
|
|
58
|
+
return envelope
|
|
59
|
+
|
|
60
|
+
def create_repository(
|
|
61
|
+
self,
|
|
62
|
+
namespace: str,
|
|
63
|
+
name: str,
|
|
64
|
+
description: str | None = None,
|
|
65
|
+
full_description: str | None = None,
|
|
66
|
+
registry: str = "docker",
|
|
67
|
+
is_private: bool = False,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""Create an image repository in a namespace."""
|
|
70
|
+
model = RepositoryCreateModel(
|
|
71
|
+
namespace=namespace,
|
|
72
|
+
name=name,
|
|
73
|
+
description=description,
|
|
74
|
+
full_description=full_description,
|
|
75
|
+
registry=registry,
|
|
76
|
+
is_private=is_private,
|
|
77
|
+
)
|
|
78
|
+
envelope = self._request(
|
|
79
|
+
"POST",
|
|
80
|
+
f"/v2/namespaces/{model.namespace}/repositories",
|
|
81
|
+
json=model.payload,
|
|
82
|
+
)
|
|
83
|
+
envelope["data"] = validate_lenient(Repository, envelope["data"])
|
|
84
|
+
return envelope
|
|
85
|
+
|
|
86
|
+
def get_repository(self, namespace: str, repository: str) -> dict[str, Any]:
|
|
87
|
+
"""Get one repository."""
|
|
88
|
+
model = RepositoryModel(namespace=namespace, repository=repository)
|
|
89
|
+
envelope = self._request(
|
|
90
|
+
"GET", f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
91
|
+
)
|
|
92
|
+
envelope["data"] = validate_lenient(Repository, envelope["data"])
|
|
93
|
+
return envelope
|
|
94
|
+
|
|
95
|
+
def check_repository(self, namespace: str, repository: str) -> dict[str, Any]:
|
|
96
|
+
"""HEAD existence check for a repository."""
|
|
97
|
+
model = RepositoryModel(namespace=namespace, repository=repository)
|
|
98
|
+
return self._exists(
|
|
99
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# ------------------------------- tags -------------------------------- #
|
|
103
|
+
|
|
104
|
+
def get_repository_tags(
|
|
105
|
+
self,
|
|
106
|
+
namespace: str,
|
|
107
|
+
repository: str,
|
|
108
|
+
page: int | None = None,
|
|
109
|
+
page_size: int | None = None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""List a repository's tags (paginated)."""
|
|
112
|
+
model = TagListModel(
|
|
113
|
+
namespace=namespace, repository=repository, page=page, page_size=page_size
|
|
114
|
+
)
|
|
115
|
+
envelope = self._request(
|
|
116
|
+
"GET",
|
|
117
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}/tags",
|
|
118
|
+
params=model.api_parameters,
|
|
119
|
+
)
|
|
120
|
+
envelope["data"] = validate_lenient(TagPage, envelope["data"])
|
|
121
|
+
return envelope
|
|
122
|
+
|
|
123
|
+
def check_repository_tags(self, namespace: str, repository: str) -> dict[str, Any]:
|
|
124
|
+
"""HEAD check: does the repository have any tags?"""
|
|
125
|
+
model = RepositoryModel(namespace=namespace, repository=repository)
|
|
126
|
+
return self._exists(
|
|
127
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}/tags"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def get_repository_tag(
|
|
131
|
+
self, namespace: str, repository: str, tag: str
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Get one tag."""
|
|
134
|
+
model = TagModel(namespace=namespace, repository=repository, tag=tag)
|
|
135
|
+
envelope = self._request(
|
|
136
|
+
"GET",
|
|
137
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
138
|
+
f"/tags/{model.tag}",
|
|
139
|
+
)
|
|
140
|
+
envelope["data"] = validate_lenient(Tag, envelope["data"])
|
|
141
|
+
return envelope
|
|
142
|
+
|
|
143
|
+
def check_repository_tag(
|
|
144
|
+
self, namespace: str, repository: str, tag: str
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""HEAD existence check for one tag."""
|
|
147
|
+
model = TagModel(namespace=namespace, repository=repository, tag=tag)
|
|
148
|
+
return self._exists(
|
|
149
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
150
|
+
f"/tags/{model.tag}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# --------------------------- immutable tags --------------------------- #
|
|
154
|
+
|
|
155
|
+
def update_immutable_tags(
|
|
156
|
+
self,
|
|
157
|
+
namespace: str,
|
|
158
|
+
repository: str,
|
|
159
|
+
enabled: bool,
|
|
160
|
+
rules: list[str] | None = None,
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
"""Patch a repository's immutable-tags settings."""
|
|
163
|
+
model = ImmutableTagsPatchModel(
|
|
164
|
+
namespace=namespace, repository=repository, enabled=enabled, rules=rules
|
|
165
|
+
)
|
|
166
|
+
envelope = self._request(
|
|
167
|
+
"PATCH",
|
|
168
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
169
|
+
"/immutabletags",
|
|
170
|
+
json=model.payload,
|
|
171
|
+
)
|
|
172
|
+
envelope["data"] = validate_lenient(ImmutableTagsSettings, envelope["data"])
|
|
173
|
+
return envelope
|
|
174
|
+
|
|
175
|
+
def verify_immutable_tags(
|
|
176
|
+
self,
|
|
177
|
+
namespace: str,
|
|
178
|
+
repository: str,
|
|
179
|
+
rules: list[str] | None = None,
|
|
180
|
+
tags: list[str] | None = None,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Verify immutable-tag rules without applying them."""
|
|
183
|
+
model = ImmutableTagsVerifyModel(
|
|
184
|
+
namespace=namespace, repository=repository, rules=rules, tags=tags
|
|
185
|
+
)
|
|
186
|
+
envelope = self._request(
|
|
187
|
+
"POST",
|
|
188
|
+
f"/v2/namespaces/{model.namespace}/repositories/{model.repository}"
|
|
189
|
+
"/immutabletags/verify",
|
|
190
|
+
json=model.payload,
|
|
191
|
+
)
|
|
192
|
+
envelope["data"] = validate_lenient(ImmutableTagsVerification, envelope["data"])
|
|
193
|
+
return envelope
|
|
194
|
+
|
|
195
|
+
# -------------------------- team permissions -------------------------- #
|
|
196
|
+
|
|
197
|
+
def assign_repository_group(
|
|
198
|
+
self,
|
|
199
|
+
namespace: str,
|
|
200
|
+
repository: str,
|
|
201
|
+
group_id: int | str,
|
|
202
|
+
permission: str,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Grant a team (group) ``read``/``write``/``admin`` on a repository."""
|
|
205
|
+
model = RepositoryGroupModel(
|
|
206
|
+
namespace=namespace,
|
|
207
|
+
repository=repository,
|
|
208
|
+
group_id=group_id,
|
|
209
|
+
permission=permission,
|
|
210
|
+
)
|
|
211
|
+
envelope = self._request(
|
|
212
|
+
"POST",
|
|
213
|
+
f"/v2/repositories/{model.namespace}/{model.repository}/groups",
|
|
214
|
+
json=model.payload,
|
|
215
|
+
)
|
|
216
|
+
envelope["data"] = validate_lenient(RepositoryGroup, envelope["data"])
|
|
217
|
+
return envelope
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""SCIM 2.0 endpoints (``/v2/scim/2.0``).
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.5 — SCIM provisioning. All requests/responses use the
|
|
4
|
+
``application/scim+json`` media type and SCIM-style 1-based pagination
|
|
5
|
+
(``startIndex``/``count``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from dockerhub_api.api.api_client_base import SCIM_CONTENT_TYPE, DockerHubApiBase
|
|
11
|
+
from dockerhub_api.dockerhub_input_models import (
|
|
12
|
+
ScimUserCreateModel,
|
|
13
|
+
ScimUserListModel,
|
|
14
|
+
ScimUserModel,
|
|
15
|
+
ScimUserReplaceModel,
|
|
16
|
+
)
|
|
17
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
18
|
+
ScimListResponse,
|
|
19
|
+
ScimResourceType,
|
|
20
|
+
ScimSchema,
|
|
21
|
+
ScimServiceProviderConfig,
|
|
22
|
+
ScimUser,
|
|
23
|
+
validate_lenient,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
SCIM_BASE = "/v2/scim/2.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DockerHubApiScim(DockerHubApiBase):
|
|
30
|
+
"""SCIM service discovery and user provisioning."""
|
|
31
|
+
|
|
32
|
+
def _scim_request(
|
|
33
|
+
self,
|
|
34
|
+
method: str,
|
|
35
|
+
endpoint: str,
|
|
36
|
+
params: dict | None = None,
|
|
37
|
+
json: Any | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
return self._request(
|
|
40
|
+
method,
|
|
41
|
+
endpoint,
|
|
42
|
+
params=params,
|
|
43
|
+
json=json,
|
|
44
|
+
content_type=SCIM_CONTENT_TYPE,
|
|
45
|
+
accept=SCIM_CONTENT_TYPE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# ----------------------------- discovery ----------------------------- #
|
|
49
|
+
|
|
50
|
+
def get_scim_service_provider_config(self) -> dict[str, Any]:
|
|
51
|
+
"""Get the SCIM ServiceProviderConfig."""
|
|
52
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/ServiceProviderConfig")
|
|
53
|
+
envelope["data"] = validate_lenient(ScimServiceProviderConfig, envelope["data"])
|
|
54
|
+
return envelope
|
|
55
|
+
|
|
56
|
+
def get_scim_resource_types(self) -> dict[str, Any]:
|
|
57
|
+
"""List the SCIM ResourceTypes."""
|
|
58
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/ResourceTypes")
|
|
59
|
+
envelope["data"] = validate_lenient(ScimListResponse, envelope["data"])
|
|
60
|
+
return envelope
|
|
61
|
+
|
|
62
|
+
def get_scim_resource_type(self, name: str) -> dict[str, Any]:
|
|
63
|
+
"""Get one SCIM ResourceType by name."""
|
|
64
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/ResourceTypes/{name}")
|
|
65
|
+
envelope["data"] = validate_lenient(ScimResourceType, envelope["data"])
|
|
66
|
+
return envelope
|
|
67
|
+
|
|
68
|
+
def get_scim_schemas(self) -> dict[str, Any]:
|
|
69
|
+
"""List the SCIM Schemas."""
|
|
70
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/Schemas")
|
|
71
|
+
envelope["data"] = validate_lenient(ScimListResponse, envelope["data"])
|
|
72
|
+
return envelope
|
|
73
|
+
|
|
74
|
+
def get_scim_schema(self, schema_id: str) -> dict[str, Any]:
|
|
75
|
+
"""Get one SCIM Schema by id (URN)."""
|
|
76
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/Schemas/{schema_id}")
|
|
77
|
+
envelope["data"] = validate_lenient(ScimSchema, envelope["data"])
|
|
78
|
+
return envelope
|
|
79
|
+
|
|
80
|
+
# ------------------------------- users ------------------------------- #
|
|
81
|
+
|
|
82
|
+
def get_scim_users(
|
|
83
|
+
self,
|
|
84
|
+
start_index: int | None = None,
|
|
85
|
+
count: int | None = None,
|
|
86
|
+
filter: str | None = None,
|
|
87
|
+
sort_by: str | None = None,
|
|
88
|
+
sort_order: str | None = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""List SCIM users (``startIndex``/``count``/``filter``/``sortBy``/``sortOrder``)."""
|
|
91
|
+
model = ScimUserListModel(
|
|
92
|
+
start_index=start_index,
|
|
93
|
+
count=count,
|
|
94
|
+
filter=filter,
|
|
95
|
+
sort_by=sort_by,
|
|
96
|
+
sort_order=sort_order,
|
|
97
|
+
)
|
|
98
|
+
envelope = self._scim_request(
|
|
99
|
+
"GET", f"{SCIM_BASE}/Users", params=model.api_parameters
|
|
100
|
+
)
|
|
101
|
+
envelope["data"] = validate_lenient(ScimListResponse, envelope["data"])
|
|
102
|
+
return envelope
|
|
103
|
+
|
|
104
|
+
def create_scim_user(
|
|
105
|
+
self,
|
|
106
|
+
user_name: str,
|
|
107
|
+
given_name: str | None = None,
|
|
108
|
+
family_name: str | None = None,
|
|
109
|
+
email: str | None = None,
|
|
110
|
+
active: bool = True,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Provision a SCIM user."""
|
|
113
|
+
model = ScimUserCreateModel(
|
|
114
|
+
user_name=user_name,
|
|
115
|
+
given_name=given_name,
|
|
116
|
+
family_name=family_name,
|
|
117
|
+
email=email,
|
|
118
|
+
active=active,
|
|
119
|
+
)
|
|
120
|
+
envelope = self._scim_request("POST", f"{SCIM_BASE}/Users", json=model.payload)
|
|
121
|
+
envelope["data"] = validate_lenient(ScimUser, envelope["data"])
|
|
122
|
+
return envelope
|
|
123
|
+
|
|
124
|
+
def get_scim_user(self, user_id: str) -> dict[str, Any]:
|
|
125
|
+
"""Get one SCIM user by id."""
|
|
126
|
+
model = ScimUserModel(user_id=user_id)
|
|
127
|
+
envelope = self._scim_request("GET", f"{SCIM_BASE}/Users/{model.user_id}")
|
|
128
|
+
envelope["data"] = validate_lenient(ScimUser, envelope["data"])
|
|
129
|
+
return envelope
|
|
130
|
+
|
|
131
|
+
def replace_scim_user(
|
|
132
|
+
self,
|
|
133
|
+
user_id: str,
|
|
134
|
+
user_name: str,
|
|
135
|
+
given_name: str | None = None,
|
|
136
|
+
family_name: str | None = None,
|
|
137
|
+
email: str | None = None,
|
|
138
|
+
active: bool = True,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Replace a SCIM user resource (``PUT``)."""
|
|
141
|
+
model = ScimUserReplaceModel(
|
|
142
|
+
user_id=user_id,
|
|
143
|
+
user_name=user_name,
|
|
144
|
+
given_name=given_name,
|
|
145
|
+
family_name=family_name,
|
|
146
|
+
email=email,
|
|
147
|
+
active=active,
|
|
148
|
+
)
|
|
149
|
+
envelope = self._scim_request(
|
|
150
|
+
"PUT", f"{SCIM_BASE}/Users/{model.user_id}", json=model.payload
|
|
151
|
+
)
|
|
152
|
+
envelope["data"] = validate_lenient(ScimUser, envelope["data"])
|
|
153
|
+
return envelope
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The composed Docker Hub API client.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.0 — core wrapper. ``Api`` is assembled from per-domain mixins,
|
|
4
|
+
all sharing the transport/auth/rate-limit plumbing in
|
|
5
|
+
:class:`~dockerhub_api.api.api_client_base.DockerHubApiBase`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from agent_utilities.base_utilities import get_logger
|
|
9
|
+
|
|
10
|
+
from dockerhub_api.api.api_client_access_tokens import DockerHubApiAccessTokens
|
|
11
|
+
from dockerhub_api.api.api_client_audit_logs import DockerHubApiAuditLogs
|
|
12
|
+
from dockerhub_api.api.api_client_auth import DockerHubApiAuth
|
|
13
|
+
from dockerhub_api.api.api_client_groups import DockerHubApiGroups
|
|
14
|
+
from dockerhub_api.api.api_client_org_access_tokens import DockerHubApiOrgAccessTokens
|
|
15
|
+
from dockerhub_api.api.api_client_orgs import DockerHubApiOrgs
|
|
16
|
+
from dockerhub_api.api.api_client_repositories import DockerHubApiRepositories
|
|
17
|
+
from dockerhub_api.api.api_client_scim import DockerHubApiScim
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Api(
|
|
23
|
+
DockerHubApiAuth,
|
|
24
|
+
DockerHubApiAccessTokens,
|
|
25
|
+
DockerHubApiOrgAccessTokens,
|
|
26
|
+
DockerHubApiAuditLogs,
|
|
27
|
+
DockerHubApiOrgs,
|
|
28
|
+
DockerHubApiRepositories,
|
|
29
|
+
DockerHubApiGroups,
|
|
30
|
+
DockerHubApiScim,
|
|
31
|
+
):
|
|
32
|
+
"""Full Docker Hub API surface: auth, tokens, repos, orgs, teams,
|
|
33
|
+
audit logs, and SCIM."""
|
|
34
|
+
|
|
35
|
+
__slots__ = ()
|