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,80 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__all__: list[str] = []
|
|
8
|
+
|
|
9
|
+
CORE_MODULES: list[str] = [
|
|
10
|
+
"dockerhub_api.dockerhub_input_models",
|
|
11
|
+
"dockerhub_api.dockerhub_response_models",
|
|
12
|
+
"dockerhub_api.api_client",
|
|
13
|
+
"dockerhub_api.auth",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
OPTIONAL_MODULES = {
|
|
17
|
+
"dockerhub_api.agent_server": "agent",
|
|
18
|
+
"dockerhub_api.mcp_server": "mcp",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _expose_members(module):
|
|
23
|
+
"""Expose public classes and functions from a module into globals and __all__."""
|
|
24
|
+
for name, obj in inspect.getmembers(module):
|
|
25
|
+
if (inspect.isclass(obj) or inspect.isfunction(obj)) and not name.startswith(
|
|
26
|
+
"_"
|
|
27
|
+
):
|
|
28
|
+
globals()[name] = obj
|
|
29
|
+
if name not in __all__:
|
|
30
|
+
__all__.append(name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Eagerly import core modules (keeps API wrappers fast & light)
|
|
34
|
+
for module_name in CORE_MODULES:
|
|
35
|
+
if module_name:
|
|
36
|
+
module = importlib.import_module(module_name)
|
|
37
|
+
_expose_members(module)
|
|
38
|
+
|
|
39
|
+
# Dynamic/lazy loading of optional modules (agent_server, mcp_server)
|
|
40
|
+
_loaded_optional_modules: dict[str, Any] = {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _import_module_safely(module_name: str):
|
|
44
|
+
"""Try to import a module and return it, or None if not available."""
|
|
45
|
+
try:
|
|
46
|
+
return importlib.import_module(module_name)
|
|
47
|
+
except ImportError:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name: str) -> Any:
|
|
52
|
+
# Handle availability flags dynamically without eager imports
|
|
53
|
+
if name == "_MCP_AVAILABLE":
|
|
54
|
+
mcp_key = next((k for k in OPTIONAL_MODULES if "mcp_server" in k), None)
|
|
55
|
+
if mcp_key:
|
|
56
|
+
return _import_module_safely(mcp_key) is not None
|
|
57
|
+
return False
|
|
58
|
+
if name == "_AGENT_AVAILABLE":
|
|
59
|
+
agent_key = next((k for k in OPTIONAL_MODULES if "agent_server" in k), None)
|
|
60
|
+
if agent_key:
|
|
61
|
+
return _import_module_safely(agent_key) is not None
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# Check optional modules
|
|
65
|
+
for module_name in OPTIONAL_MODULES:
|
|
66
|
+
if module_name not in _loaded_optional_modules:
|
|
67
|
+
module = _import_module_safely(module_name)
|
|
68
|
+
if module is not None:
|
|
69
|
+
_loaded_optional_modules[module_name] = module
|
|
70
|
+
_expose_members(module)
|
|
71
|
+
|
|
72
|
+
module = _loaded_optional_modules.get(module_name)
|
|
73
|
+
if module is not None and hasattr(module, name):
|
|
74
|
+
return getattr(module, name)
|
|
75
|
+
|
|
76
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def __dir__() -> list[str]:
|
|
80
|
+
return sorted(list(globals().keys()) + __all__)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
"""Docker Hub A2A agent server.
|
|
3
|
+
|
|
4
|
+
CONCEPT:HUB-1.6 — A2A agent server. A Pydantic-AI graph agent over the
|
|
5
|
+
Docker Hub MCP tool surface, exposed via agent-utilities'
|
|
6
|
+
``create_agent_server`` (A2A + AG-UI web interface).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.INFO,
|
|
18
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
19
|
+
handlers=[logging.StreamHandler()],
|
|
20
|
+
)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_AGENT_NAME = None
|
|
25
|
+
DEFAULT_AGENT_DESCRIPTION = None
|
|
26
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def agent_server():
|
|
30
|
+
from agent_utilities import (
|
|
31
|
+
build_system_prompt_from_workspace,
|
|
32
|
+
create_agent_parser,
|
|
33
|
+
create_agent_server,
|
|
34
|
+
initialize_workspace,
|
|
35
|
+
load_identity,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
global DEFAULT_AGENT_NAME, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_SYSTEM_PROMPT
|
|
39
|
+
initialize_workspace()
|
|
40
|
+
meta = load_identity()
|
|
41
|
+
DEFAULT_AGENT_NAME = os.getenv(
|
|
42
|
+
"DEFAULT_AGENT_NAME", meta.get("name", "Dockerhub Api")
|
|
43
|
+
)
|
|
44
|
+
DEFAULT_AGENT_DESCRIPTION = os.getenv(
|
|
45
|
+
"AGENT_DESCRIPTION",
|
|
46
|
+
meta.get(
|
|
47
|
+
"description",
|
|
48
|
+
"AI agent for Docker Hub management.",
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
|
|
52
|
+
"AGENT_SYSTEM_PROMPT",
|
|
53
|
+
meta.get("content") or build_system_prompt_from_workspace(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
|
|
57
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
|
|
58
|
+
|
|
59
|
+
print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
|
|
60
|
+
parser = create_agent_parser()
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
if args.debug:
|
|
64
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
65
|
+
logger.debug("Debug mode enabled")
|
|
66
|
+
|
|
67
|
+
# Start server using the auto-discovery pattern (from mcp_config.json)
|
|
68
|
+
create_agent_server(
|
|
69
|
+
mcp_url=args.mcp_url,
|
|
70
|
+
mcp_config=args.mcp_config or "mcp_config.json",
|
|
71
|
+
host=args.host,
|
|
72
|
+
port=args.port,
|
|
73
|
+
provider=args.provider,
|
|
74
|
+
model_id=args.model_id,
|
|
75
|
+
router_model=args.model_id,
|
|
76
|
+
agent_model=args.model_id,
|
|
77
|
+
base_url=args.base_url,
|
|
78
|
+
api_key=args.api_key,
|
|
79
|
+
custom_skills_directory=args.custom_skills_directory,
|
|
80
|
+
enable_web_ui=args.web,
|
|
81
|
+
enable_otel=args.otel,
|
|
82
|
+
otel_endpoint=args.otel_endpoint,
|
|
83
|
+
otel_headers=args.otel_headers,
|
|
84
|
+
otel_public_key=args.otel_public_key,
|
|
85
|
+
otel_secret_key=args.otel_secret_key,
|
|
86
|
+
otel_protocol=args.otel_protocol,
|
|
87
|
+
debug=args.debug,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
agent_server()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-domain Docker Hub API client mixins."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Personal access token endpoints (``/v2/access-tokens``).
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.1 — JWT auth lifecycle (PAT management).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
9
|
+
from dockerhub_api.dockerhub_input_models import (
|
|
10
|
+
AccessTokenCreateModel,
|
|
11
|
+
AccessTokenListModel,
|
|
12
|
+
AccessTokenModel,
|
|
13
|
+
AccessTokenPatchModel,
|
|
14
|
+
)
|
|
15
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
16
|
+
AccessToken,
|
|
17
|
+
AccessTokenPage,
|
|
18
|
+
validate_lenient,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DockerHubApiAccessTokens(DockerHubApiBase):
|
|
23
|
+
"""CRUD for personal access tokens."""
|
|
24
|
+
|
|
25
|
+
def get_access_tokens(
|
|
26
|
+
self, page: int | None = None, page_size: int | None = None
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""List the personal access tokens of the authenticated user."""
|
|
29
|
+
model = AccessTokenListModel(page=page, page_size=page_size)
|
|
30
|
+
envelope = self._request(
|
|
31
|
+
"GET", "/v2/access-tokens", params=model.api_parameters
|
|
32
|
+
)
|
|
33
|
+
envelope["data"] = validate_lenient(AccessTokenPage, envelope["data"])
|
|
34
|
+
return envelope
|
|
35
|
+
|
|
36
|
+
def create_access_token(
|
|
37
|
+
self, token_label: str, scopes: list[str]
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Create a personal access token.
|
|
40
|
+
|
|
41
|
+
Valid scopes: ``repo:admin``, ``repo:write``, ``repo:read``,
|
|
42
|
+
``repo:public_read``. The plaintext token is only returned once,
|
|
43
|
+
in this response.
|
|
44
|
+
"""
|
|
45
|
+
model = AccessTokenCreateModel(token_label=token_label, scopes=scopes)
|
|
46
|
+
envelope = self._request("POST", "/v2/access-tokens", json=model.payload)
|
|
47
|
+
envelope["data"] = validate_lenient(AccessToken, envelope["data"])
|
|
48
|
+
return envelope
|
|
49
|
+
|
|
50
|
+
def get_access_token(self, uuid: str) -> dict[str, Any]:
|
|
51
|
+
"""Get one personal access token by UUID."""
|
|
52
|
+
model = AccessTokenModel(uuid=uuid)
|
|
53
|
+
envelope = self._request("GET", f"/v2/access-tokens/{model.uuid}")
|
|
54
|
+
envelope["data"] = validate_lenient(AccessToken, envelope["data"])
|
|
55
|
+
return envelope
|
|
56
|
+
|
|
57
|
+
def update_access_token(
|
|
58
|
+
self,
|
|
59
|
+
uuid: str,
|
|
60
|
+
token_label: str | None = None,
|
|
61
|
+
is_active: bool | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Patch a personal access token's label and/or active state."""
|
|
64
|
+
model = AccessTokenPatchModel(
|
|
65
|
+
uuid=uuid, token_label=token_label, is_active=is_active
|
|
66
|
+
)
|
|
67
|
+
envelope = self._request(
|
|
68
|
+
"PATCH", f"/v2/access-tokens/{model.uuid}", json=model.payload
|
|
69
|
+
)
|
|
70
|
+
envelope["data"] = validate_lenient(AccessToken, envelope["data"])
|
|
71
|
+
return envelope
|
|
72
|
+
|
|
73
|
+
def delete_access_token(self, uuid: str) -> dict[str, Any]:
|
|
74
|
+
"""Delete a personal access token. Destructive — gated."""
|
|
75
|
+
self._guard_destructive("delete_access_token")
|
|
76
|
+
model = AccessTokenModel(uuid=uuid)
|
|
77
|
+
return self._request("DELETE", f"/v2/access-tokens/{model.uuid}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Audit log endpoints (``/v2/auditlogs``).
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.0 — core wrapper.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
9
|
+
from dockerhub_api.dockerhub_input_models import AuditLogModel
|
|
10
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
11
|
+
AuditLogActions,
|
|
12
|
+
AuditLogPage,
|
|
13
|
+
validate_lenient,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DockerHubApiAuditLogs(DockerHubApiBase):
|
|
18
|
+
"""Read access to an account's audit trail."""
|
|
19
|
+
|
|
20
|
+
def get_audit_logs(
|
|
21
|
+
self,
|
|
22
|
+
account: str,
|
|
23
|
+
action: str | None = None,
|
|
24
|
+
name: str | None = None,
|
|
25
|
+
actor: str | None = None,
|
|
26
|
+
from_date: str | None = None,
|
|
27
|
+
to_date: str | None = None,
|
|
28
|
+
page: int | None = None,
|
|
29
|
+
page_size: int | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""List audit-log events for a namespace.
|
|
32
|
+
|
|
33
|
+
Filters: ``action``, ``name`` (object), ``actor`` (username), and a
|
|
34
|
+
``from_date``/``to_date`` RFC 3339 window (sent as ``from``/``to``).
|
|
35
|
+
"""
|
|
36
|
+
model = AuditLogModel(
|
|
37
|
+
account=account,
|
|
38
|
+
action=action,
|
|
39
|
+
name=name,
|
|
40
|
+
actor=actor,
|
|
41
|
+
from_date=from_date,
|
|
42
|
+
to_date=to_date,
|
|
43
|
+
page=page,
|
|
44
|
+
page_size=page_size,
|
|
45
|
+
)
|
|
46
|
+
envelope = self._request(
|
|
47
|
+
"GET", f"/v2/auditlogs/{model.account}", params=model.api_parameters
|
|
48
|
+
)
|
|
49
|
+
envelope["data"] = validate_lenient(AuditLogPage, envelope["data"])
|
|
50
|
+
return envelope
|
|
51
|
+
|
|
52
|
+
def get_audit_log_actions(self, account: str) -> dict[str, Any]:
|
|
53
|
+
"""List the audit-log action names available for a namespace."""
|
|
54
|
+
envelope = self._request("GET", f"/v2/auditlogs/{account}/actions")
|
|
55
|
+
envelope["data"] = validate_lenient(AuditLogActions, envelope["data"])
|
|
56
|
+
return envelope
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Docker Hub authentication endpoints.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.1 — JWT auth lifecycle (endpoint wrappers).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
10
|
+
from dockerhub_api.dockerhub_input_models import (
|
|
11
|
+
AuthTokenModel,
|
|
12
|
+
LoginModel,
|
|
13
|
+
TwoFactorLoginModel,
|
|
14
|
+
)
|
|
15
|
+
from dockerhub_api.dockerhub_response_models import (
|
|
16
|
+
JwtToken,
|
|
17
|
+
LoginResult,
|
|
18
|
+
validate_lenient,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DockerHubApiAuth(DockerHubApiBase):
|
|
23
|
+
"""``/v2/auth/token``, ``/v2/users/login``, ``/v2/users/2fa-login``."""
|
|
24
|
+
|
|
25
|
+
def create_auth_token(self, identifier: str, secret: str) -> dict[str, Any]:
|
|
26
|
+
"""Mint a short-lived JWT bearer from an identifier + secret.
|
|
27
|
+
|
|
28
|
+
The secret may be an account password, a personal access token
|
|
29
|
+
(``dckr_pat_*``), or an organization access token.
|
|
30
|
+
"""
|
|
31
|
+
model = AuthTokenModel(identifier=identifier, secret=secret)
|
|
32
|
+
envelope = self._request("POST", "/v2/auth/token", json=model.payload)
|
|
33
|
+
envelope["data"] = validate_lenient(JwtToken, envelope["data"])
|
|
34
|
+
return envelope
|
|
35
|
+
|
|
36
|
+
def login(self, username: str, password: str) -> dict[str, Any]:
|
|
37
|
+
"""Authenticate via the legacy login endpoint.
|
|
38
|
+
|
|
39
|
+
.. deprecated:: 0.1.0
|
|
40
|
+
``POST /v2/users/login`` is deprecated by Docker Hub — prefer
|
|
41
|
+
:meth:`create_auth_token` (``POST /v2/auth/token``). Kept for
|
|
42
|
+
parity with the published API surface and for the 2FA flow.
|
|
43
|
+
"""
|
|
44
|
+
warnings.warn(
|
|
45
|
+
"POST /v2/users/login is deprecated by Docker Hub; "
|
|
46
|
+
"use create_auth_token (POST /v2/auth/token) instead.",
|
|
47
|
+
DeprecationWarning,
|
|
48
|
+
stacklevel=2,
|
|
49
|
+
)
|
|
50
|
+
model = LoginModel(username=username, password=password)
|
|
51
|
+
envelope = self._request(
|
|
52
|
+
"POST", "/v2/users/login", json=model.payload, raise_for_status=False
|
|
53
|
+
)
|
|
54
|
+
# A 401 carrying a login_2fa_token is the expected second-factor
|
|
55
|
+
# challenge, not an error.
|
|
56
|
+
data = envelope["data"]
|
|
57
|
+
if envelope["status_code"] >= 400 and not (
|
|
58
|
+
isinstance(data, dict) and data.get("login_2fa_token")
|
|
59
|
+
):
|
|
60
|
+
self._raise_for_status_envelope(envelope)
|
|
61
|
+
envelope["data"] = validate_lenient(LoginResult, data)
|
|
62
|
+
return envelope
|
|
63
|
+
|
|
64
|
+
def two_factor_login(self, login_2fa_token: str, code: str) -> dict[str, Any]:
|
|
65
|
+
"""Complete a 2FA login with the TOTP code (``POST /v2/users/2fa-login``)."""
|
|
66
|
+
model = TwoFactorLoginModel(login_2fa_token=login_2fa_token, code=code)
|
|
67
|
+
envelope = self._request("POST", "/v2/users/2fa-login", json=model.payload)
|
|
68
|
+
envelope["data"] = validate_lenient(LoginResult, envelope["data"])
|
|
69
|
+
return envelope
|
|
70
|
+
|
|
71
|
+
def _raise_for_status_envelope(self, envelope: dict[str, Any]) -> None:
|
|
72
|
+
from agent_utilities.core.exceptions import ApiError, AuthError
|
|
73
|
+
|
|
74
|
+
status_code = envelope["status_code"]
|
|
75
|
+
detail = ""
|
|
76
|
+
if isinstance(envelope["data"], dict):
|
|
77
|
+
detail = str(envelope["data"].get("detail") or "")
|
|
78
|
+
if status_code in (401, 403):
|
|
79
|
+
raise AuthError(f"Login failed (HTTP {status_code}): {detail}")
|
|
80
|
+
raise ApiError(f"Login failed (HTTP {status_code}): {detail}")
|