deepquery-sdk 1.0.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,101 @@
1
+ """DeepQuerySDK — the Connector Development Kit for Deep Query.
2
+
3
+ Build a connector by subclassing `Connector` and declaring reads with
4
+ `@resource` and writes with `@action`. The connector emits a standard MCP
5
+ server (see `deepquery_sdk.mcp_emit`).
6
+
7
+ See SDK_GUIDE.md for the full specification.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .action import ActionSpec, action
13
+ from .auth import (
14
+ ApiKeyAuth,
15
+ AppliedAuth,
16
+ AuthStrategy,
17
+ BasicAuth,
18
+ BearerAuth,
19
+ Credential,
20
+ CredentialError,
21
+ CredentialProvider,
22
+ MTLSAuth,
23
+ OAuth2Auth,
24
+ PKCEPair,
25
+ build_authorization_url,
26
+ build_token_request,
27
+ env_credential_provider,
28
+ generate_pkce,
29
+ static_credential_provider,
30
+ )
31
+ from .classification import CapabilityKind
32
+ from .connector import Connector, ConnectorContractError
33
+ from .gate import ActionStatus, ApprovalGate, GateError, PendingAction
34
+ from .compat import (
35
+ CompatibilityResult,
36
+ IncompatibleConnectorError,
37
+ assert_compatible,
38
+ check_manifest_compatibility,
39
+ )
40
+ from .manifest import (
41
+ ActionManifest,
42
+ AuthMethod,
43
+ DeploymentCompat,
44
+ Manifest,
45
+ ResourceManifest,
46
+ SDK_MAJOR_VERSION,
47
+ SDK_VERSION,
48
+ )
49
+ from .provenance import CitedRecord, Provenance, make_provenance, now_iso
50
+ from .resource import ResourceSpec, resource
51
+
52
+ __version__ = SDK_VERSION
53
+
54
+ __all__ = [
55
+ "Connector",
56
+ "ConnectorContractError",
57
+ "resource",
58
+ "ResourceSpec",
59
+ "action",
60
+ "ActionSpec",
61
+ "CapabilityKind",
62
+ "CitedRecord",
63
+ "Provenance",
64
+ "make_provenance",
65
+ "now_iso",
66
+ "Manifest",
67
+ "ResourceManifest",
68
+ "ActionManifest",
69
+ "AuthMethod",
70
+ "DeploymentCompat",
71
+ "SDK_MAJOR_VERSION",
72
+ "SDK_VERSION",
73
+ # compatibility
74
+ "assert_compatible",
75
+ "check_manifest_compatibility",
76
+ "CompatibilityResult",
77
+ "IncompatibleConnectorError",
78
+ # auth
79
+ "AuthStrategy",
80
+ "Credential",
81
+ "CredentialError",
82
+ "CredentialProvider",
83
+ "AppliedAuth",
84
+ "ApiKeyAuth",
85
+ "BasicAuth",
86
+ "BearerAuth",
87
+ "OAuth2Auth",
88
+ "MTLSAuth",
89
+ "PKCEPair",
90
+ "generate_pkce",
91
+ "build_authorization_url",
92
+ "build_token_request",
93
+ "static_credential_provider",
94
+ "env_credential_provider",
95
+ # gate
96
+ "ApprovalGate",
97
+ "ActionStatus",
98
+ "PendingAction",
99
+ "GateError",
100
+ "__version__",
101
+ ]
@@ -0,0 +1,104 @@
1
+ """Action definitions — the write/mutation side of a connector.
2
+
3
+ An action is a state-mutating capability. It is declared with the `@action`
4
+ decorator on a `Connector` method (the *execute* step), and its mandatory
5
+ *preview* is attached with `@<action>.preview`. The separation of preview and
6
+ execute is the architectural heart of the approval-gate safety model: the agent
7
+ and the gate always see the preview first, and execute is only ever called after
8
+ human confirmation.
9
+
10
+ In Phase 1 the SDK captures and validates this contract and emits the action
11
+ into the MCP server tagged `dq.mutates: true`. The full gated
12
+ `preview → approve → execute` drive is wired through the harness in Phase 2; an
13
+ emitted action invoked directly returns its preview and does **not** execute.
14
+
15
+ See SDK_GUIDE.md §4.3 and §5.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Callable
21
+
22
+ from .classification import CapabilityKind
23
+
24
+
25
+ class ActionSpec:
26
+ """Declarative metadata + bound functions for one action.
27
+
28
+ Becomes the class attribute in place of the decorated method, so
29
+ `@<action>.preview` can register the preview step at class-body time.
30
+ """
31
+
32
+ kind = CapabilityKind.ACTION
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ name: str | None,
38
+ description: str,
39
+ input_schema: dict[str, Any],
40
+ ) -> None:
41
+ self._name = name
42
+ self.description = description
43
+ self.input_schema = input_schema
44
+ self.execute_fn: Callable | None = None
45
+ self.preview_fn: Callable | None = None
46
+ self.attr_name: str | None = None
47
+
48
+ # -- decorator wiring -------------------------------------------------
49
+ def __call__(self, fn: Callable) -> "ActionSpec":
50
+ """Capture the execute function (the body of `@action(...)`)."""
51
+ self.execute_fn = fn
52
+ if self._name is None:
53
+ self._name = fn.__name__
54
+ return self
55
+
56
+ def preview(self, fn: Callable) -> Callable:
57
+ """Register the mandatory preview function.
58
+
59
+ Usage::
60
+
61
+ @create_issue.preview
62
+ def _(self, **params) -> str: ...
63
+
64
+ Returns the preview function unchanged so it does not shadow the action
65
+ attribute on the class.
66
+ """
67
+ self.preview_fn = fn
68
+ return fn
69
+
70
+ def __set_name__(self, owner: type, attr_name: str) -> None:
71
+ self.attr_name = attr_name
72
+
73
+ # -- accessors --------------------------------------------------------
74
+ @property
75
+ def name(self) -> str:
76
+ # _name is set either explicitly or from the execute fn in __call__.
77
+ assert self._name is not None, "action defined without an execute function"
78
+ return self._name
79
+
80
+ def has_preview(self) -> bool:
81
+ return self.preview_fn is not None
82
+
83
+ def __repr__(self) -> str: # pragma: no cover - debug aid
84
+ return f"ActionSpec(name={self._name!r}, has_preview={self.has_preview()})"
85
+
86
+
87
+ def action(
88
+ *,
89
+ description: str,
90
+ name: str | None = None,
91
+ input_schema: dict[str, Any] | None = None,
92
+ ) -> ActionSpec:
93
+ """Declare a state-mutating action.
94
+
95
+ Decorate the *execute* method with `@action(...)`, then attach the mandatory
96
+ preview with `@<action>.preview`. A connector that declares an action without
97
+ a preview is a contract violation and is rejected when the connector class is
98
+ defined.
99
+ """
100
+ return ActionSpec(
101
+ name=name,
102
+ description=description,
103
+ input_schema=input_schema or {"type": "object", "properties": {}},
104
+ )
@@ -0,0 +1,45 @@
1
+ """Authentication scaffolding for connectors.
2
+
3
+ Connectors declare an `AuthStrategy` and read an injected `Credential` per call;
4
+ they never store credentials. See SDK_GUIDE.md §7.
5
+ """
6
+
7
+ from .base import (
8
+ AppliedAuth,
9
+ AuthStrategy,
10
+ Credential,
11
+ CredentialError,
12
+ CredentialProvider,
13
+ env_credential_provider,
14
+ static_credential_provider,
15
+ )
16
+ from .oauth2 import (
17
+ PKCEPair,
18
+ TokenRequest,
19
+ build_authorization_url,
20
+ build_refresh_request,
21
+ build_token_request,
22
+ generate_pkce,
23
+ )
24
+ from .strategies import ApiKeyAuth, BasicAuth, BearerAuth, MTLSAuth, OAuth2Auth
25
+
26
+ __all__ = [
27
+ "AppliedAuth",
28
+ "AuthStrategy",
29
+ "Credential",
30
+ "CredentialError",
31
+ "CredentialProvider",
32
+ "static_credential_provider",
33
+ "env_credential_provider",
34
+ "ApiKeyAuth",
35
+ "BasicAuth",
36
+ "BearerAuth",
37
+ "OAuth2Auth",
38
+ "MTLSAuth",
39
+ "PKCEPair",
40
+ "generate_pkce",
41
+ "build_authorization_url",
42
+ "build_token_request",
43
+ "build_refresh_request",
44
+ "TokenRequest",
45
+ ]
@@ -0,0 +1,96 @@
1
+ """Auth foundations: credentials, strategies, and the injection hook.
2
+
3
+ Connectors authenticate to external systems but **never store credentials**
4
+ (SDK_GUIDE.md §7). The gateway owns credential lifecycle and injects a valid
5
+ credential at call time through a `CredentialProvider`. An `AuthStrategy`
6
+ declares *how* to attach that credential to outbound requests; it never holds the
7
+ secret itself — it receives one per call and returns the headers/cert to apply.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import ABC, abstractmethod
13
+ from typing import Any, Callable, Optional
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+ from ..manifest import AuthMethod
18
+
19
+
20
+ class Credential(BaseModel):
21
+ """Opaque secret material injected by the gateway for a single call.
22
+
23
+ Only the fields relevant to the connector's declared auth method are set.
24
+ The connector reads this via `self.current_credential` during a call and must
25
+ never persist it.
26
+ """
27
+
28
+ token: Optional[str] = None # OAuth bearer token or API key value
29
+ username: Optional[str] = None
30
+ password: Optional[str] = None
31
+ client_cert_path: Optional[str] = None
32
+ client_key_path: Optional[str] = None
33
+ extra: dict[str, Any] = Field(default_factory=dict)
34
+
35
+
36
+ class AppliedAuth(BaseModel):
37
+ """The result of applying a strategy to a credential: what to attach to a request."""
38
+
39
+ headers: dict[str, str] = Field(default_factory=dict)
40
+ # httpx-style client cert: a (cert, key) path tuple, when using mTLS.
41
+ cert: Optional[tuple[str, str]] = None
42
+
43
+
44
+ class CredentialError(Exception):
45
+ """Raised when a credential is required for a call but none was injected."""
46
+
47
+
48
+ # The gateway supplies one of these. It is called per request and may return a
49
+ # different (e.g. per-user) credential each time. Returning None means "no
50
+ # credential available" — strategies that require one will raise.
51
+ CredentialProvider = Callable[[], Optional[Credential]]
52
+
53
+
54
+ class AuthStrategy(ABC):
55
+ """Declares how a connector attaches an injected credential to its requests."""
56
+
57
+ method: AuthMethod
58
+
59
+ @abstractmethod
60
+ def apply(self, credential: Credential) -> AppliedAuth:
61
+ """Return the headers / cert to attach, given an injected credential."""
62
+ raise NotImplementedError
63
+
64
+ @property
65
+ def scopes(self) -> list[str]:
66
+ """OAuth scopes this strategy requests (empty for non-OAuth)."""
67
+ return []
68
+
69
+
70
+ def static_credential_provider(credential: Credential) -> CredentialProvider:
71
+ """A provider that always returns the same credential.
72
+
73
+ Used for air-gapped / static injection (API keys, mTLS certs) sourced from
74
+ the institution's own secret store with no external OAuth round-trip
75
+ (SDK_GUIDE.md §7).
76
+ """
77
+
78
+ def _provider() -> Credential:
79
+ return credential
80
+
81
+ return _provider
82
+
83
+
84
+ def env_credential_provider(*, token_env: str) -> CredentialProvider:
85
+ """A provider that reads a bearer token / API key from an environment variable.
86
+
87
+ Convenient for self-hosted, air-gapped connectors whose secret store exposes
88
+ credentials through the process environment.
89
+ """
90
+ import os
91
+
92
+ def _provider() -> Optional[Credential]:
93
+ value = os.environ.get(token_env)
94
+ return Credential(token=value) if value else None
95
+
96
+ return _provider
@@ -0,0 +1,106 @@
1
+ """OAuth 2.1 flow-construction scaffolding (authorization code + PKCE).
2
+
3
+ These are the pure building blocks a gateway uses to run the authorization-code
4
+ flow with PKCE: generate a PKCE pair, build the authorization URL, and build the
5
+ token-exchange request. The SDK intentionally does **not** perform token storage,
6
+ exchange-over-the-wire, or refresh — credential lifecycle is owned by the gateway
7
+ (SDK_GUIDE.md §7). Keeping these helpers pure also makes them testable offline.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import hashlib
14
+ import secrets
15
+ from dataclasses import dataclass
16
+ from urllib.parse import urlencode
17
+
18
+
19
+ def _b64url(data: bytes) -> str:
20
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class PKCEPair:
25
+ """A PKCE code verifier and its S256-derived challenge."""
26
+
27
+ verifier: str
28
+ challenge: str
29
+ method: str = "S256"
30
+
31
+
32
+ def generate_pkce() -> PKCEPair:
33
+ """Generate a PKCE verifier/challenge pair (RFC 7636, S256)."""
34
+ verifier = _b64url(secrets.token_bytes(32)) # 43-char high-entropy verifier
35
+ challenge = _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
36
+ return PKCEPair(verifier=verifier, challenge=challenge)
37
+
38
+
39
+ def build_authorization_url(
40
+ *,
41
+ authorize_endpoint: str,
42
+ client_id: str,
43
+ redirect_uri: str,
44
+ scopes: list[str],
45
+ code_challenge: str,
46
+ state: str | None = None,
47
+ code_challenge_method: str = "S256",
48
+ ) -> str:
49
+ """Build the authorization-request URL the user is redirected to."""
50
+ params = {
51
+ "response_type": "code",
52
+ "client_id": client_id,
53
+ "redirect_uri": redirect_uri,
54
+ "scope": " ".join(scopes),
55
+ "code_challenge": code_challenge,
56
+ "code_challenge_method": code_challenge_method,
57
+ }
58
+ if state is not None:
59
+ params["state"] = state
60
+ return f"{authorize_endpoint}?{urlencode(params)}"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class TokenRequest:
65
+ """A ready-to-send token-exchange request (the gateway performs the POST)."""
66
+
67
+ url: str
68
+ data: dict[str, str]
69
+
70
+
71
+ def build_token_request(
72
+ *,
73
+ token_endpoint: str,
74
+ client_id: str,
75
+ code: str,
76
+ code_verifier: str,
77
+ redirect_uri: str,
78
+ ) -> TokenRequest:
79
+ """Build the authorization-code → token exchange request body."""
80
+ return TokenRequest(
81
+ url=token_endpoint,
82
+ data={
83
+ "grant_type": "authorization_code",
84
+ "client_id": client_id,
85
+ "code": code,
86
+ "code_verifier": code_verifier,
87
+ "redirect_uri": redirect_uri,
88
+ },
89
+ )
90
+
91
+
92
+ def build_refresh_request(
93
+ *,
94
+ token_endpoint: str,
95
+ client_id: str,
96
+ refresh_token: str,
97
+ ) -> TokenRequest:
98
+ """Build a refresh-token request (gateway performs and stores the result)."""
99
+ return TokenRequest(
100
+ url=token_endpoint,
101
+ data={
102
+ "grant_type": "refresh_token",
103
+ "client_id": client_id,
104
+ "refresh_token": refresh_token,
105
+ },
106
+ )
@@ -0,0 +1,91 @@
1
+ """Concrete auth strategies: API key, basic, bearer/OAuth2, mTLS.
2
+
3
+ Each strategy receives an injected `Credential` per call and returns the headers
4
+ (or client cert) to attach. None of them store the credential.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+
11
+ from ..manifest import AuthMethod
12
+ from .base import AppliedAuth, AuthStrategy, Credential, CredentialError
13
+
14
+
15
+ class ApiKeyAuth(AuthStrategy):
16
+ """Attach an API key as a header, e.g. `Authorization: Bearer <key>` or `X-API-Key: <key>`."""
17
+
18
+ method = AuthMethod.API_KEY
19
+
20
+ def __init__(self, *, header_name: str = "Authorization", value_prefix: str = "") -> None:
21
+ self.header_name = header_name
22
+ self.value_prefix = value_prefix
23
+
24
+ def apply(self, credential: Credential) -> AppliedAuth:
25
+ if not credential.token:
26
+ raise CredentialError("ApiKeyAuth requires credential.token")
27
+ value = f"{self.value_prefix}{credential.token}" if self.value_prefix else credential.token
28
+ return AppliedAuth(headers={self.header_name: value})
29
+
30
+
31
+ class BasicAuth(AuthStrategy):
32
+ """HTTP Basic auth from an injected username/password."""
33
+
34
+ method = AuthMethod.BASIC
35
+
36
+ def apply(self, credential: Credential) -> AppliedAuth:
37
+ if credential.username is None or credential.password is None:
38
+ raise CredentialError("BasicAuth requires credential.username and credential.password")
39
+ raw = f"{credential.username}:{credential.password}".encode()
40
+ encoded = base64.b64encode(raw).decode()
41
+ return AppliedAuth(headers={"Authorization": f"Basic {encoded}"})
42
+
43
+
44
+ class BearerAuth(AuthStrategy):
45
+ """Attach a bearer token (the shape an OAuth access token takes on requests)."""
46
+
47
+ method = AuthMethod.OAUTH2
48
+
49
+ def apply(self, credential: Credential) -> AppliedAuth:
50
+ if not credential.token:
51
+ raise CredentialError("BearerAuth requires credential.token")
52
+ return AppliedAuth(headers={"Authorization": f"Bearer {credential.token}"})
53
+
54
+
55
+ class OAuth2Auth(BearerAuth):
56
+ """OAuth 2.1 strategy: declares endpoints + scopes, attaches the bearer token.
57
+
58
+ The connector declares the flow's endpoints and the minimum scopes it needs
59
+ (least-privilege, SDK_GUIDE.md §13). The gateway runs and stores the grant
60
+ (§7); see `deepquery_sdk.auth.oauth2` for the PKCE flow-construction helpers.
61
+ """
62
+
63
+ method = AuthMethod.OAUTH2
64
+
65
+ def __init__(
66
+ self,
67
+ *,
68
+ authorize_endpoint: str,
69
+ token_endpoint: str,
70
+ scopes: list[str] | None = None,
71
+ ) -> None:
72
+ self.authorize_endpoint = authorize_endpoint
73
+ self.token_endpoint = token_endpoint
74
+ self._scopes = list(scopes or [])
75
+
76
+ @property
77
+ def scopes(self) -> list[str]:
78
+ return self._scopes
79
+
80
+
81
+ class MTLSAuth(AuthStrategy):
82
+ """Mutual TLS: attach an injected client certificate/key pair."""
83
+
84
+ method = AuthMethod.MTLS
85
+
86
+ def apply(self, credential: Credential) -> AppliedAuth:
87
+ if not credential.client_cert_path or not credential.client_key_path:
88
+ raise CredentialError(
89
+ "MTLSAuth requires credential.client_cert_path and credential.client_key_path"
90
+ )
91
+ return AppliedAuth(cert=(credential.client_cert_path, credential.client_key_path))
@@ -0,0 +1,43 @@
1
+ """Read / Action classification — the safety contract.
2
+
3
+ Every capability a connector exposes is classified *at definition time* as
4
+ either a read (Resource) or an action (mutating tool). This classification is
5
+ declared by the developer, never inferred at runtime, and is embedded in the
6
+ emitted MCP server as metadata the Connector Infrastructure layer reads to
7
+ decide gating.
8
+
9
+ See SDK_GUIDE.md §5.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from enum import Enum
15
+
16
+
17
+ class CapabilityKind(str, Enum):
18
+ """How a capability is classified for the approval gate."""
19
+
20
+ RESOURCE = "resource" # read-only, ungated, citeable
21
+ ACTION = "action" # mutates external state, gated behind human approval
22
+
23
+
24
+ # Metadata keys attached to the emitted MCP `Tool._meta`. The Connector
25
+ # Infrastructure gateway reads these to decide gating and citation handling.
26
+ # Namespaced under `dq.` so they never collide with other MCP metadata.
27
+ DQ_MUTATES = "dq.mutates" # bool — True for actions, False for resources
28
+ DQ_KIND = "dq.kind" # CapabilityKind value
29
+ DQ_CONNECTOR = "dq.connector" # originating connector name
30
+
31
+
32
+ def mutates(kind: CapabilityKind) -> bool:
33
+ """Whether a capability of this kind mutates external state."""
34
+ return kind is CapabilityKind.ACTION
35
+
36
+
37
+ def meta_for(kind: CapabilityKind, connector_name: str) -> dict[str, object]:
38
+ """Build the `dq.*` metadata block for a capability's emitted MCP tool."""
39
+ return {
40
+ DQ_KIND: kind.value,
41
+ DQ_MUTATES: mutates(kind),
42
+ DQ_CONNECTOR: connector_name,
43
+ }
@@ -0,0 +1,5 @@
1
+ """DeepQuerySDK command-line tooling (scaffold / validate / run-dev / emit / manifest)."""
2
+
3
+ from .__main__ import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,64 @@
1
+ """The `deepquery` command-line entry point.
2
+
3
+ Built on stdlib argparse — no third-party CLI dependency — so the SDK stays a
4
+ thin install (mcp + pydantic).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+
12
+ from . import commands
13
+
14
+ _TARGET_HELP = (
15
+ "connector target: 'module.path:ClassName', a module, or a path to a "
16
+ "connector .py file (or a directory containing connector.py)"
17
+ )
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(prog="deepquery", description="DeepQuerySDK connector tooling.")
22
+ sub = parser.add_subparsers(dest="command", required=True)
23
+
24
+ p_scaffold = sub.add_parser("scaffold", help="generate a new connector from the template")
25
+ p_scaffold.add_argument("name", help="connector name, e.g. 'jira' or 'acme-crm'")
26
+ p_scaffold.add_argument("--dir", help="output directory (default: ./<name>)")
27
+ p_scaffold.set_defaults(func=commands.cmd_scaffold)
28
+
29
+ p_validate = sub.add_parser("validate", help="static-check a connector against the safety contracts")
30
+ p_validate.add_argument("target", help=_TARGET_HELP)
31
+ p_validate.set_defaults(func=commands.cmd_validate)
32
+
33
+ p_manifest = sub.add_parser("manifest", help="print or export the connector manifest")
34
+ p_manifest.add_argument("target", help=_TARGET_HELP)
35
+ p_manifest.add_argument("--out", help="write manifest JSON to this file instead of stdout")
36
+ p_manifest.set_defaults(func=commands.cmd_manifest)
37
+
38
+ p_emit = sub.add_parser("emit", help="produce the deployable MCP server artifact")
39
+ p_emit.add_argument("target", help=_TARGET_HELP)
40
+ p_emit.add_argument("--out", default="dist", help="output directory (default: ./dist)")
41
+ p_emit.set_defaults(func=commands.cmd_emit)
42
+
43
+ p_run = sub.add_parser("run-dev", help="drive the connector locally with the mock agent")
44
+ p_run.add_argument("target", help=_TARGET_HELP)
45
+ p_run.set_defaults(func=commands.cmd_run_dev)
46
+
47
+ return parser
48
+
49
+
50
+ def main(argv: list[str] | None = None) -> int:
51
+ # The guide references sections with '§' and uses em dashes; make sure those
52
+ # render on legacy Windows consoles (cp1252) rather than showing as '�'.
53
+ for stream in (sys.stdout, sys.stderr):
54
+ try:
55
+ stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
56
+ except (AttributeError, ValueError):
57
+ pass
58
+ parser = build_parser()
59
+ args = parser.parse_args(argv)
60
+ return args.func(args)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(main())