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.
@@ -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__ = ()