gosset 0.3.2__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.
- gosset/__init__.py +34 -0
- gosset/auth.py +261 -0
- gosset/cli.py +151 -0
- gosset/client.py +933 -0
- gosset/entities.py +263 -0
- gosset/output.py +95 -0
- gosset/resolve.py +118 -0
- gosset-0.3.2.dist-info/METADATA +272 -0
- gosset-0.3.2.dist-info/RECORD +13 -0
- gosset-0.3.2.dist-info/WHEEL +5 -0
- gosset-0.3.2.dist-info/entry_points.txt +3 -0
- gosset-0.3.2.dist-info/licenses/LICENSE +202 -0
- gosset-0.3.2.dist-info/top_level.txt +1 -0
gosset/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gosset CLI & SDK — command-line and programmatic access to Gosset's drug,
|
|
3
|
+
trial, company, deal and news data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Single source of truth: the version lives in pyproject.toml and is read back
|
|
7
|
+
# from the installed distribution metadata. It used to be duplicated here as a
|
|
8
|
+
# literal, so a release could ship a package whose __version__ disagreed with
|
|
9
|
+
# its own filename and nothing would catch it.
|
|
10
|
+
#
|
|
11
|
+
# The fallback covers running from a source checkout that was never installed —
|
|
12
|
+
# there is no distribution metadata to read then, and a wrong-but-plausible
|
|
13
|
+
# number would be worse than an obviously-unreleased one.
|
|
14
|
+
#
|
|
15
|
+
# Caveat, verified rather than assumed: this resolves by DISTRIBUTION NAME
|
|
16
|
+
# across the environment, not by the source tree being imported. Importing
|
|
17
|
+
# from a checkout while a different gosset is installed reports the INSTALLED
|
|
18
|
+
# version (observed: a source tree reporting 0.2.3 from an unrelated env).
|
|
19
|
+
# That is the accepted trade of this idiom: it guarantees the property that
|
|
20
|
+
# matters — a published artifact's __version__ is, by construction, the
|
|
21
|
+
# version in its own filename — at the cost of ambiguity in dev trees.
|
|
22
|
+
from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
|
|
23
|
+
from importlib.metadata import version as _distribution_version
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
__version__ = _distribution_version("gosset")
|
|
27
|
+
except _PackageNotFoundError: # pragma: no cover - source tree, not installed
|
|
28
|
+
__version__ = "0.0.0.dev0"
|
|
29
|
+
|
|
30
|
+
from .auth import get_oauth_token
|
|
31
|
+
from .client import GossetClient, GossetAPIError
|
|
32
|
+
|
|
33
|
+
__all__ = ["get_oauth_token", "GossetClient", "GossetAPIError", "__version__"]
|
|
34
|
+
|
gosset/auth.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth authentication for Gosset API
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import webbrowser
|
|
8
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
9
|
+
from urllib.parse import parse_qs, urlparse
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
API_BASE_URL = os.environ.get("OAUTH_BASE_URL", "https://api.gosset.ai")
|
|
15
|
+
REDIRECT_URI = "http://localhost:8765/callback"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
19
|
+
"""Handle OAuth callback"""
|
|
20
|
+
auth_code = None
|
|
21
|
+
|
|
22
|
+
def do_GET(self):
|
|
23
|
+
# Parse query parameters
|
|
24
|
+
query = urlparse(self.path).query
|
|
25
|
+
params = parse_qs(query)
|
|
26
|
+
|
|
27
|
+
if 'code' in params:
|
|
28
|
+
CallbackHandler.auth_code = params['code'][0]
|
|
29
|
+
self.send_response(200)
|
|
30
|
+
self.send_header('Content-type', 'text/html')
|
|
31
|
+
self.end_headers()
|
|
32
|
+
self.wfile.write(b"""
|
|
33
|
+
<html><body>
|
|
34
|
+
<h1>Authorization successful!</h1>
|
|
35
|
+
<p>You can close this window and return to the terminal.</p>
|
|
36
|
+
</body></html>
|
|
37
|
+
""")
|
|
38
|
+
else:
|
|
39
|
+
self.send_response(400)
|
|
40
|
+
self.send_header('Content-type', 'text/html')
|
|
41
|
+
self.end_headers()
|
|
42
|
+
error = params.get('error', ['Unknown error'])[0]
|
|
43
|
+
self.wfile.write(f"<html><body><h1>Authorization failed: {error}</h1></body></html>".encode())
|
|
44
|
+
|
|
45
|
+
def log_message(self, format, *args):
|
|
46
|
+
pass # Suppress logging
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_oauth_token(base_url=None, quiet=False):
|
|
50
|
+
"""
|
|
51
|
+
Get OAuth token through interactive browser flow
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
base_url: Optional custom API base URL
|
|
55
|
+
quiet: If True, suppress informational output
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: OAuth access token if successful, None otherwise
|
|
59
|
+
"""
|
|
60
|
+
if base_url is None:
|
|
61
|
+
base_url = API_BASE_URL
|
|
62
|
+
|
|
63
|
+
if not quiet:
|
|
64
|
+
print("=" * 70)
|
|
65
|
+
print("Interactive OAuth Token Acquisition")
|
|
66
|
+
print("=" * 70)
|
|
67
|
+
print()
|
|
68
|
+
|
|
69
|
+
# Step 1: Register a client
|
|
70
|
+
if not quiet:
|
|
71
|
+
print("Step 1: Registering OAuth client...")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
register_response = requests.post(
|
|
75
|
+
f"{base_url}/oauth/register",
|
|
76
|
+
json={"redirect_uris": [REDIRECT_URI]},
|
|
77
|
+
headers={"Content-Type": "application/json"}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if register_response.status_code != 201:
|
|
81
|
+
if not quiet:
|
|
82
|
+
print(f"✗ Failed to register client: {register_response.text}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
client_data = register_response.json()
|
|
86
|
+
client_id = client_data["client_id"]
|
|
87
|
+
client_secret = client_data["client_secret"]
|
|
88
|
+
|
|
89
|
+
if not quiet:
|
|
90
|
+
print(f"✓ Client registered: {client_id}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
if not quiet:
|
|
93
|
+
print(f"✗ Error registering client: {e}")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Step 2: Start local server for callback
|
|
97
|
+
if not quiet:
|
|
98
|
+
print("\nStep 2: Starting local callback server...")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
server = HTTPServer(('localhost', 8765), CallbackHandler)
|
|
102
|
+
except OSError as e:
|
|
103
|
+
# The commonest way to hit this is running `gosset get-key` twice: the
|
|
104
|
+
# first is still holding the port while it waits for the browser, so the
|
|
105
|
+
# second dies here. An unhandled OSError traceback is a poor answer to
|
|
106
|
+
# what is really "you already have one of these running".
|
|
107
|
+
if not quiet:
|
|
108
|
+
print(f"\u2717 Could not start the callback server on port 8765: {e}")
|
|
109
|
+
print(" Another `gosset get-key`/`get-token` is probably still waiting")
|
|
110
|
+
print(" for authorization. Finish or cancel it, then retry.")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
|
114
|
+
server_thread.daemon = True
|
|
115
|
+
server_thread.start()
|
|
116
|
+
|
|
117
|
+
if not quiet:
|
|
118
|
+
print(f"✓ Callback server running on {REDIRECT_URI}")
|
|
119
|
+
|
|
120
|
+
# Step 3: Open browser for authorization
|
|
121
|
+
if not quiet:
|
|
122
|
+
print("\nStep 3: Opening browser for authorization...")
|
|
123
|
+
|
|
124
|
+
# Build authorization URL
|
|
125
|
+
auth_url = f"{base_url}/oauth/authorize"
|
|
126
|
+
params = {
|
|
127
|
+
"response_type": "code",
|
|
128
|
+
"client_id": client_id,
|
|
129
|
+
"redirect_uri": REDIRECT_URI,
|
|
130
|
+
"scope": "read write",
|
|
131
|
+
"state": "test123"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
auth_url_with_params = f"{auth_url}?" + "&".join([f"{k}={v}" for k, v in params.items()])
|
|
135
|
+
|
|
136
|
+
if not quiet:
|
|
137
|
+
print(f"Authorization URL: {auth_url_with_params}")
|
|
138
|
+
print("\nOpening browser... (if it doesn't open, copy the URL above)")
|
|
139
|
+
|
|
140
|
+
webbrowser.open(auth_url_with_params)
|
|
141
|
+
|
|
142
|
+
# Wait for callback
|
|
143
|
+
if not quiet:
|
|
144
|
+
print("\nWaiting for authorization...")
|
|
145
|
+
|
|
146
|
+
timeout = 120 # 2 minutes
|
|
147
|
+
start_time = time.time()
|
|
148
|
+
|
|
149
|
+
while CallbackHandler.auth_code is None and (time.time() - start_time) < timeout:
|
|
150
|
+
time.sleep(0.5)
|
|
151
|
+
|
|
152
|
+
server.shutdown()
|
|
153
|
+
|
|
154
|
+
if not CallbackHandler.auth_code:
|
|
155
|
+
if not quiet:
|
|
156
|
+
print(f"✗ No authorization received within {timeout}s")
|
|
157
|
+
print(" The browser must complete the flow on THIS machine — the")
|
|
158
|
+
print(" callback server listens on its localhost:8765, so opening the")
|
|
159
|
+
print(" URL elsewhere cannot reach it. On a headless host this can")
|
|
160
|
+
print(" never succeed; run it where you have a browser.")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if not quiet:
|
|
164
|
+
print(f"✓ Authorization code received")
|
|
165
|
+
|
|
166
|
+
# Step 4: Exchange code for token
|
|
167
|
+
if not quiet:
|
|
168
|
+
print("\nStep 4: Exchanging code for access token...")
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
token_response = requests.post(
|
|
172
|
+
f"{base_url}/oauth/token",
|
|
173
|
+
data={
|
|
174
|
+
"grant_type": "authorization_code",
|
|
175
|
+
"code": CallbackHandler.auth_code,
|
|
176
|
+
"client_id": client_id,
|
|
177
|
+
"client_secret": client_secret,
|
|
178
|
+
"redirect_uri": REDIRECT_URI
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if token_response.status_code != 200:
|
|
183
|
+
if not quiet:
|
|
184
|
+
print(f"✗ Failed to get token: {token_response.text}")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
token_data = token_response.json()
|
|
188
|
+
access_token = token_data["access_token"]
|
|
189
|
+
|
|
190
|
+
if not quiet:
|
|
191
|
+
print("\n" + "=" * 70)
|
|
192
|
+
print("SUCCESS! Your OAuth token:")
|
|
193
|
+
print("=" * 70)
|
|
194
|
+
print(f"\n{access_token}\n")
|
|
195
|
+
print("To use this token, set it as an environment variable:")
|
|
196
|
+
print(f"export GOSSET_API_KEY='{access_token}'")
|
|
197
|
+
print("\nOr add to your ~/.bashrc or ~/.zshrc")
|
|
198
|
+
print("\nNote: GOSSET_OAUTH_TOKEN also works for backward compatibility")
|
|
199
|
+
print("=" * 70)
|
|
200
|
+
|
|
201
|
+
return access_token
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
if not quiet:
|
|
205
|
+
print(f"✗ Error getting token: {e}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def get_api_key(base_url=None, quiet=False):
|
|
209
|
+
"""Run the OAuth browser flow, then exchange the result for a durable API key.
|
|
210
|
+
|
|
211
|
+
`get_oauth_token` returns an OAuth ACCESS TOKEN, which the REST API cannot
|
|
212
|
+
accept: its auth validates a bearer against accounts.users by token/api_key
|
|
213
|
+
and never consults the OAuth store. So the token that flow produces fails on
|
|
214
|
+
every `gosset drugs`-style call, with an unhelpful "Authentication failed".
|
|
215
|
+
|
|
216
|
+
The OAuth flow is still the right way to establish WHO you are — it is the
|
|
217
|
+
only credential a CLI can obtain from a browser unattended. This adds the
|
|
218
|
+
missing second step: hand that identity to /api_key/from_oauth/ and get back
|
|
219
|
+
the key the API actually accepts. One command, one credential.
|
|
220
|
+
"""
|
|
221
|
+
import os
|
|
222
|
+
|
|
223
|
+
import requests
|
|
224
|
+
|
|
225
|
+
base = (base_url or os.environ.get("GOSSET_API_URL")
|
|
226
|
+
or "https://api.gosset.ai").rstrip("/")
|
|
227
|
+
|
|
228
|
+
token = get_oauth_token(base_url=base_url, quiet=quiet)
|
|
229
|
+
if not token:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
if not quiet:
|
|
233
|
+
print("\nExchanging for a durable API key...")
|
|
234
|
+
try:
|
|
235
|
+
resp = requests.post(
|
|
236
|
+
f"{base}/api_key/from_oauth/",
|
|
237
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
238
|
+
timeout=30,
|
|
239
|
+
)
|
|
240
|
+
except requests.RequestException as e:
|
|
241
|
+
if not quiet:
|
|
242
|
+
print(f"\u2717 Could not reach {base}: {e}")
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if resp.status_code != 200:
|
|
246
|
+
if not quiet:
|
|
247
|
+
print(f"\u2717 Exchange failed ({resp.status_code}): {resp.text[:200]}")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
api_key = (resp.json() or {}).get("api_key")
|
|
251
|
+
if not api_key:
|
|
252
|
+
if not quiet:
|
|
253
|
+
print("\u2717 Exchange returned no api_key")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
if not quiet:
|
|
257
|
+
print("\u2713 API key ready\n")
|
|
258
|
+
print(api_key)
|
|
259
|
+
print("\nSet it with:")
|
|
260
|
+
print(f" export GOSSET_API_KEY='{api_key}'")
|
|
261
|
+
return api_key
|
gosset/cli.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for the Gosset CLI.
|
|
3
|
+
|
|
4
|
+
Sub-commands:
|
|
5
|
+
drugs / trials / companies / deals / news entity search (name↔id resolved for you)
|
|
6
|
+
ptrs <nct> probability of technical & regulatory success
|
|
7
|
+
timeline <nct> predicted primary-completion timeline
|
|
8
|
+
get-key obtain an API key (recommended)
|
|
9
|
+
get-token obtain a raw OAuth token (MCP)
|
|
10
|
+
version print the version
|
|
11
|
+
|
|
12
|
+
Output is JSON by default (agent / jq friendly); pass --table for a human table.
|
|
13
|
+
Auth: --api-key or $GOSSET_API_KEY (get one with `gosset get-key`).
|
|
14
|
+
"""
|
|
15
|
+
import sys
|
|
16
|
+
import json
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
from .auth import get_api_key, get_oauth_token
|
|
20
|
+
from . import __version__
|
|
21
|
+
from .entities import add_entity_parsers
|
|
22
|
+
from .client import GossetClient, GossetAPIError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_key_command(args):
|
|
26
|
+
"""The credential users actually want: an API key the REST API accepts."""
|
|
27
|
+
key = get_api_key(base_url=args.base_url, quiet=args.quiet)
|
|
28
|
+
if key:
|
|
29
|
+
if args.quiet:
|
|
30
|
+
print(key)
|
|
31
|
+
return 0
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_token_command(args):
|
|
36
|
+
"""Raw OAuth access token — for MCP, NOT for the REST API.
|
|
37
|
+
|
|
38
|
+
Kept because it is documented and in use, but it is not what you want for
|
|
39
|
+
`gosset drugs`: the REST API validates bearers against accounts.users and
|
|
40
|
+
rejects OAuth tokens, so this one authenticates nothing here. `get-key`
|
|
41
|
+
runs the same browser flow and returns the usable credential.
|
|
42
|
+
"""
|
|
43
|
+
token = get_oauth_token(base_url=args.base_url, quiet=args.quiet)
|
|
44
|
+
if token:
|
|
45
|
+
if args.quiet:
|
|
46
|
+
print(token)
|
|
47
|
+
elif not args.quiet:
|
|
48
|
+
print("\nNote: this is an OAuth token, for MCP clients.")
|
|
49
|
+
print("For the CLI and SDK use `gosset get-key` instead.")
|
|
50
|
+
return 0
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def version_command(args):
|
|
55
|
+
print(f"Gosset CLI v{__version__}")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ptrs_command(client, args):
|
|
60
|
+
res = client.estimate_ptrs({"nct_id": args.nct_id})
|
|
61
|
+
print(json.dumps(res, indent=2, default=str))
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def timeline_command(client, args):
|
|
66
|
+
res = client.estimate_remaining_time(args.nct_id, as_of=args.as_of)
|
|
67
|
+
print(json.dumps(res, indent=2, default=str))
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _auth_parent():
|
|
72
|
+
"""Shared auth flags applied to every command that needs the API."""
|
|
73
|
+
p = argparse.ArgumentParser(add_help=False)
|
|
74
|
+
p.add_argument("--api-key", default=None,
|
|
75
|
+
help="Bearer token (default: $GOSSET_API_KEY)")
|
|
76
|
+
p.add_argument("--base-url", default=None,
|
|
77
|
+
help="API base URL (default: https://api.gosset.ai or $GOSSET_API_URL)")
|
|
78
|
+
return p
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
"""Main CLI entry point."""
|
|
83
|
+
parser = argparse.ArgumentParser(
|
|
84
|
+
description="Gosset CLI — command-line access to Gosset drugs, trials, companies, deals, news.",
|
|
85
|
+
prog="gosset",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
88
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
89
|
+
|
|
90
|
+
auth = _auth_parent()
|
|
91
|
+
|
|
92
|
+
# Entity search commands: drugs / trials / companies / deals / news
|
|
93
|
+
add_entity_parsers(subparsers, parents=[auth])
|
|
94
|
+
|
|
95
|
+
# ptrs
|
|
96
|
+
pt = subparsers.add_parser("ptrs", parents=[auth],
|
|
97
|
+
help="Estimate probability of technical & regulatory success for a trial")
|
|
98
|
+
pt.add_argument("nct_id", help="ClinicalTrials.gov id, e.g. NCT05599191")
|
|
99
|
+
pt.set_defaults(func=ptrs_command, _needs_client=True)
|
|
100
|
+
|
|
101
|
+
# timeline
|
|
102
|
+
tl = subparsers.add_parser("timeline", parents=[auth],
|
|
103
|
+
help="Predict a trial's primary-completion timeline")
|
|
104
|
+
tl.add_argument("nct_id", help="ClinicalTrials.gov id")
|
|
105
|
+
tl.add_argument("--as-of", default=None, help="anchor date YYYY-MM-DD (default: today)")
|
|
106
|
+
tl.set_defaults(func=timeline_command, _needs_client=True)
|
|
107
|
+
|
|
108
|
+
# get-key — the one most users want
|
|
109
|
+
key_parser = subparsers.add_parser(
|
|
110
|
+
"get-key", help="Get an API key for the Gosset API (recommended)")
|
|
111
|
+
key_parser.add_argument("--quiet", action="store_true",
|
|
112
|
+
help="Only output the key")
|
|
113
|
+
key_parser.add_argument("--base-url", help="API base URL")
|
|
114
|
+
key_parser.set_defaults(func=get_key_command, _needs_client=False)
|
|
115
|
+
|
|
116
|
+
# get-token
|
|
117
|
+
token_parser = subparsers.add_parser("get-token", help="Get a raw OAuth token (MCP clients)")
|
|
118
|
+
token_parser.add_argument("--base-url", help="Custom API base URL (optional)")
|
|
119
|
+
token_parser.add_argument("--quiet", action="store_true", help="Only output the token")
|
|
120
|
+
token_parser.set_defaults(func=get_token_command, _needs_client=False)
|
|
121
|
+
|
|
122
|
+
# version
|
|
123
|
+
ver = subparsers.add_parser("version", help="Print the Gosset CLI version")
|
|
124
|
+
ver.set_defaults(func=version_command, _needs_client=False)
|
|
125
|
+
|
|
126
|
+
args = parser.parse_args()
|
|
127
|
+
|
|
128
|
+
if not getattr(args, "command", None):
|
|
129
|
+
parser.print_help()
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
# Commands that hit the API need an authenticated client.
|
|
133
|
+
if getattr(args, "_needs_client", False):
|
|
134
|
+
try:
|
|
135
|
+
client = GossetClient(api_key=getattr(args, "api_key", None),
|
|
136
|
+
base_url=getattr(args, "base_url", None))
|
|
137
|
+
except ValueError:
|
|
138
|
+
print("Error: no API key. Run `gosset get-key` or set GOSSET_API_KEY "
|
|
139
|
+
"(or pass --api-key).", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
try:
|
|
142
|
+
return args.func(client, args)
|
|
143
|
+
except GossetAPIError as e:
|
|
144
|
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
return args.func(args)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
sys.exit(main())
|