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,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,4 @@
1
+ from dockerhub_api.agent_server import agent_server
2
+
3
+ if __name__ == "__main__":
4
+ agent_server()
@@ -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}")