Coverage for pyngrok/ngrok.py: 87.21%

219 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-13 02:08 +0000

1import json 

2import logging 

3import os 

4import socket 

5import sys 

6import uuid 

7from http import HTTPStatus 

8from urllib.error import HTTPError, URLError 

9from urllib.parse import urlencode 

10from urllib.request import urlopen, Request 

11 

12from pyngrok import process, conf, installer 

13from pyngrok.exception import PyngrokNgrokHTTPError, PyngrokNgrokURLError, PyngrokSecurityError, PyngrokError 

14 

15__author__ = "Alex Laird" 

16__copyright__ = "Copyright 2023, Alex Laird" 

17__version__ = "6.1.0" 

18 

19from pyngrok.installer import get_default_config 

20 

21logger = logging.getLogger(__name__) 

22 

23_current_tunnels = {} 

24 

25 

26class NgrokTunnel: 

27 """ 

28 An object containing information about a ``ngrok`` tunnel. 

29 

30 :var data: The original tunnel data. 

31 :vartype data: dict 

32 :var name: The name of the tunnel. 

33 :vartype name: str 

34 :var proto: The protocol of the tunnel. 

35 :vartype proto: str 

36 :var uri: The tunnel URI, a relative path that can be used to make requests to the ``ngrok`` web interface. 

37 :vartype uri: str 

38 :var public_url: The public ``ngrok`` URL. 

39 :vartype public_url: str 

40 :var config: The config for the tunnel. 

41 :vartype config: dict 

42 :var metrics: Metrics for `the tunnel <https://ngrok.com/docs/ngrok-agent/api#list-tunnels>`_. 

43 :vartype metrics: dict 

44 :var pyngrok_config: The ``pyngrok`` configuration to use when interacting with the ``ngrok``. 

45 :vartype pyngrok_config: PyngrokConfig 

46 :var api_url: The API URL for the ``ngrok`` web interface. 

47 :vartype api_url: str 

48 """ 

49 

50 def __init__(self, data, pyngrok_config, api_url): 

51 self.data = data 

52 

53 self.id = data.get("ID", None) 

54 self.name = data.get("name") 

55 self.proto = data.get("proto") 

56 self.uri = data.get("uri") 

57 self.public_url = data.get("public_url") 

58 self.config = data.get("config", {}) 

59 self.metrics = data.get("metrics", {}) 

60 

61 self.pyngrok_config = pyngrok_config 

62 self.api_url = api_url 

63 

64 def __repr__(self): 

65 return "<NgrokTunnel: \"{}\" -> \"{}\">".format(self.public_url, self.config["addr"]) if self.config.get( 

66 "addr", None) else "<pending Tunnel>" 

67 

68 def __str__(self): # pragma: no cover 

69 return "NgrokTunnel: \"{}\" -> \"{}\"".format(self.public_url, self.config["addr"]) if self.config.get( 

70 "addr", None) else "<pending Tunnel>" 

71 

72 def refresh_metrics(self): 

73 """ 

74 Get the latest metrics for the tunnel and update the ``metrics`` variable. 

75 """ 

76 logger.info("Refreshing metrics for tunnel: {}".format(self.public_url)) 

77 

78 data = api_request("{}{}".format(self.api_url, self.uri), method="GET", 

79 timeout=self.pyngrok_config.request_timeout) 

80 

81 if "metrics" not in data: 

82 raise PyngrokError("The ngrok API did not return \"metrics\" in the response") 

83 

84 self.data["metrics"] = data["metrics"] 

85 self.metrics = self.data["metrics"] 

86 

87 

88def install_ngrok(pyngrok_config=None): 

89 """ 

90 Download, install, and initialize ``ngrok`` for the given config. If ``ngrok`` and its default 

91 config is already installed, calling this method will do nothing. 

92 

93 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

94 overriding :func:`~pyngrok.conf.get_default()`. 

95 :type pyngrok_config: PyngrokConfig, optional 

96 """ 

97 if pyngrok_config is None: 

98 pyngrok_config = conf.get_default() 

99 

100 if not os.path.exists(pyngrok_config.ngrok_path): 

101 installer.install_ngrok(pyngrok_config.ngrok_path, pyngrok_config.ngrok_version) 

102 

103 # If no config_path is set, ngrok will use its default path 

104 if pyngrok_config.config_path is not None: 

105 config_path = pyngrok_config.config_path 

106 else: 

107 config_path = conf.DEFAULT_NGROK_CONFIG_PATH 

108 

109 # Install the config to the requested path 

110 if not os.path.exists(config_path): 

111 installer.install_default_config(config_path, ngrok_version=pyngrok_config.ngrok_version) 

112 

113 # Install the default config, even if we don't need it this time, if it doesn't already exist 

114 if conf.DEFAULT_NGROK_CONFIG_PATH != config_path and \ 

115 not os.path.exists(conf.DEFAULT_NGROK_CONFIG_PATH): 

116 installer.install_default_config(conf.DEFAULT_NGROK_CONFIG_PATH, ngrok_version=pyngrok_config.ngrok_version) 

117 

118 

119def set_auth_token(token, pyngrok_config=None): 

120 """ 

121 Set the ``ngrok`` auth token in the config file, enabling authenticated features (for instance, 

122 more concurrent tunnels, custom subdomains, etc.). 

123 

124 If ``ngrok`` is not installed at :class:`~pyngrok.conf.PyngrokConfig`'s ``ngrok_path``, calling this method 

125 will first download and install ``ngrok``. 

126 

127 :param token: The auth token to set. 

128 :type token: str 

129 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

130 overriding :func:`~pyngrok.conf.get_default()`. 

131 :type pyngrok_config: PyngrokConfig, optional 

132 """ 

133 if pyngrok_config is None: 

134 pyngrok_config = conf.get_default() 

135 

136 install_ngrok(pyngrok_config) 

137 

138 process.set_auth_token(pyngrok_config, token) 

139 

140 

141def get_ngrok_process(pyngrok_config=None): 

142 """ 

143 Get the current ``ngrok`` process for the given config's ``ngrok_path``. 

144 

145 If ``ngrok`` is not installed at :class:`~pyngrok.conf.PyngrokConfig`'s ``ngrok_path``, calling this method 

146 will first download and install ``ngrok``. 

147 

148 If ``ngrok`` is not running, calling this method will first start a process with 

149 :class:`~pyngrok.conf.PyngrokConfig`. 

150 

151 Use :func:`~pyngrok.process.is_process_running` to check if a process is running without also implicitly 

152 installing and starting it. 

153 

154 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

155 overriding :func:`~pyngrok.conf.get_default()`. 

156 :type pyngrok_config: PyngrokConfig, optional 

157 :return: The ``ngrok`` process. 

158 :rtype: NgrokProcess 

159 """ 

160 if pyngrok_config is None: 

161 pyngrok_config = conf.get_default() 

162 

163 install_ngrok(pyngrok_config) 

164 

165 return process.get_process(pyngrok_config) 

166 

167 

168def _apply_cloud_edge_to_tunnel(tunnel, pyngrok_config): 

169 if not tunnel.public_url and pyngrok_config.api_key and tunnel.id: 

170 tunnel_response = api_request("https://api.ngrok.com/tunnels/{}".format(tunnel.id), method="GET", 

171 auth=pyngrok_config.api_key) 

172 if "labels" not in tunnel_response or "edge" not in tunnel_response["labels"]: 

173 raise PyngrokError( 

174 "Tunnel {} does not have \"labels\", use a Tunnel configured on Cloud Edge.".format(tunnel.data["ID"])) 

175 

176 edge = tunnel_response["labels"]["edge"] 

177 if edge.startswith("edghts_"): 

178 edges_prefix = "https" 

179 elif edge.startswith("edgtcp"): 

180 edges_prefix = "tcp" 

181 elif edge.startswith("edgtls"): 

182 edges_prefix = "tls" 

183 else: 

184 raise PyngrokError("Unknown Edge prefix: {}.".format(edge)) 

185 

186 edge_response = api_request("https://api.ngrok.com/edges/{}/{}".format(edges_prefix, edge), method="GET", 

187 auth=pyngrok_config.api_key) 

188 

189 if "hostports" not in edge_response or len(edge_response["hostports"]) < 1: 

190 raise PyngrokError( 

191 "No Endpoint is attached to your Cloud Edge {}, login to the ngrok dashboard to attach an Endpoint to your Edge first.".format(edge)) 

192 

193 tunnel.public_url = "{}://{}".format(edges_prefix, edge_response["hostports"][0]) 

194 tunnel.proto = edges_prefix 

195 

196 

197def connect(addr=None, proto=None, name=None, pyngrok_config=None, **options): 

198 """ 

199 Establish a new ``ngrok`` tunnel for the given protocol to the given port, returning an object representing 

200 the connected tunnel. 

201 

202 If a `tunnel definition in ngrok's config file 

203 <https://ngrok.com/docs/secure-tunnels/ngrok-agent/reference/config/#tunnel-definitions>`_ matches the given 

204 ``name``, it will be loaded and used to start the tunnel. When ``name`` is ``None`` and a "pyngrok-default" tunnel 

205 definition exists in ``ngrok``'s config, it will be loaded and use. Any ``kwargs`` passed as ``options`` will 

206 override properties from the loaded tunnel definition. 

207 

208 If ``ngrok`` is not installed at :class:`~pyngrok.conf.PyngrokConfig`'s ``ngrok_path``, calling this method 

209 will first download and install ``ngrok``. 

210 

211 ``pyngrok`` is compatible with ``ngrok`` v2 and v3, but by default it will install v3. To install v2 instead, 

212 set ``ngrok_version`` in :class:`~pyngrok.conf.PyngrokConfig`: 

213 

214 If ``ngrok`` is not running, calling this method will first start a process with 

215 :class:`~pyngrok.conf.PyngrokConfig`. 

216 

217 .. note:: 

218 

219 ``ngrok`` v2's default behavior for ``http`` when no additional properties are passed is to open *two* tunnels, 

220 one ``http`` and one ``https``. This method will return a reference to the ``http`` tunnel in this case. If 

221 only a single tunnel is needed, pass ``bind_tls=True`` and a reference to the ``https`` tunnel will be returned. 

222 

223 :param addr: The local port to which the tunnel will forward traffic, or a 

224 `local directory or network address <https://ngrok.com/docs/secure-tunnels/tunnels/http-tunnels#file-url>`_, defaults to "80". 

225 :type addr: str, optional 

226 :param proto: A valid `tunnel protocol 

227 <https://ngrok.com/docs/secure-tunnels/ngrok-agent/reference/config/#tunnel-definitions>`_, defaults to "http". 

228 :type proto: str, optional 

229 :param name: A friendly name for the tunnel, or the name of a `ngrok tunnel definition <https://ngrok.com/docs/secure-tunnels/ngrok-agent/reference/config/#tunnel-definitions>`_ 

230 to be used. 

231 :type name: str, optional 

232 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

233 overriding :func:`~pyngrok.conf.get_default()`. 

234 :type pyngrok_config: PyngrokConfig, optional 

235 :param options: Remaining ``kwargs`` are passed as `configuration for the ngrok 

236 tunnel <https://ngrok.com/docs/secure-tunnels/ngrok-agent/reference/config/#tunnel-definitions>`_. 

237 :type options: dict, optional 

238 :return: The created ``ngrok`` tunnel. 

239 :rtype: NgrokTunnel 

240 """ 

241 if "labels" in options: 

242 raise PyngrokError("\"labels\" cannot be passed to connect(), define a tunnel definition in the config file.") 

243 

244 if pyngrok_config is None: 

245 pyngrok_config = conf.get_default() 

246 

247 if pyngrok_config.config_path is not None: 

248 config_path = pyngrok_config.config_path 

249 else: 

250 config_path = conf.DEFAULT_NGROK_CONFIG_PATH 

251 

252 if os.path.exists(config_path): 

253 config = installer.get_ngrok_config(config_path) 

254 else: 

255 config = get_default_config(pyngrok_config.ngrok_version) 

256 

257 # If a "pyngrok-default" tunnel definition exists in the ngrok config, use that 

258 tunnel_definitions = config.get("tunnels", {}) 

259 if not name and "pyngrok-default" in tunnel_definitions: 

260 name = "pyngrok-default" 

261 

262 # Use a tunnel definition for the given name, if it exists 

263 if name and name in tunnel_definitions: 

264 tunnel_definition = tunnel_definitions[name] 

265 

266 addr = tunnel_definition.get("addr") if not addr else addr 

267 proto = tunnel_definition.get("proto") if not proto else proto 

268 # Use the tunnel definition as the base, but override with any passed in options 

269 tunnel_definition.update(options) 

270 options = tunnel_definition 

271 

272 if "labels" in options and not pyngrok_config.api_key: 

273 raise PyngrokError( 

274 "\"PyngrokConfig.api_key\" must be set when \"labels\" is on the tunnel definition.") 

275 

276 addr = str(addr) if addr else "80" 

277 # Only apply a default proto label if "labels" isn't defined 

278 if not proto and "labels" not in options: 

279 proto = "http" 

280 

281 if not name: 

282 if not addr.startswith("file://"): 

283 name = "{}-{}-{}".format(proto, addr, uuid.uuid4()) 

284 else: 

285 name = "{}-file-{}".format(proto, uuid.uuid4()) 

286 

287 logger.info("Opening tunnel named: {}".format(name)) 

288 

289 config = { 

290 "name": name, 

291 "addr": addr 

292 } 

293 options.update(config) 

294 

295 # Only apply proto when "labels" is not defined 

296 if "labels" not in options: 

297 options["proto"] = proto 

298 

299 if "labels" in options and "bind_tls" in options: 

300 raise PyngrokError("\"bind_tls\" cannot be set when \"labels\" is also on the tunnel definition.") 

301 

302 # Upgrade legacy parameters, if present 

303 if pyngrok_config.ngrok_version == "v3": 

304 if "bind_tls" in options: 

305 if options.get("bind_tls") is True or options.get("bind_tls") == "true": 

306 options["schemes"] = ["https"] 

307 elif not options.get("bind_tls") is not False or options.get("bind_tls") == "false": 

308 options["schemes"] = ["http"] 

309 else: 

310 options["schemes"] = ["http", "https"] 

311 

312 options.pop("bind_tls") 

313 

314 if "auth" in options: 

315 auth = options.get("auth") 

316 if isinstance(auth, list): 

317 options["basic_auth"] = auth 

318 else: 

319 options["basic_auth"] = [auth] 

320 

321 options.pop("auth") 

322 

323 api_url = get_ngrok_process(pyngrok_config).api_url 

324 

325 logger.debug("Creating tunnel with options: {}".format(options)) 

326 

327 tunnel = NgrokTunnel(api_request("{}/api/tunnels".format(api_url), method="POST", data=options, 

328 timeout=pyngrok_config.request_timeout), 

329 pyngrok_config, api_url) 

330 

331 if pyngrok_config.ngrok_version == "v2" and proto == "http" and options.get("bind_tls", "both") == "both": 

332 tunnel = NgrokTunnel(api_request("{}{}%20%28http%29".format(api_url, tunnel.uri), method="GET", 

333 timeout=pyngrok_config.request_timeout), 

334 pyngrok_config, api_url) 

335 

336 _apply_cloud_edge_to_tunnel(tunnel, pyngrok_config) 

337 

338 _current_tunnels[tunnel.public_url] = tunnel 

339 

340 return tunnel 

341 

342 

343def disconnect(public_url, pyngrok_config=None): 

344 """ 

345 Disconnect the ``ngrok`` tunnel for the given URL, if open. 

346 

347 :param public_url: The public URL of the tunnel to disconnect. 

348 :type public_url: str 

349 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

350 overriding :func:`~pyngrok.conf.get_default()`. 

351 :type pyngrok_config: PyngrokConfig, optional 

352 """ 

353 if pyngrok_config is None: 

354 pyngrok_config = conf.get_default() 

355 

356 # If ngrok is not running, there are no tunnels to disconnect 

357 if not process.is_process_running(pyngrok_config.ngrok_path): 

358 return 

359 

360 api_url = get_ngrok_process(pyngrok_config).api_url 

361 

362 if public_url not in _current_tunnels: 

363 get_tunnels(pyngrok_config) 

364 

365 # One more check, if the given URL is still not in the list of tunnels, it is not active 

366 if public_url not in _current_tunnels: 

367 return 

368 

369 tunnel = _current_tunnels[public_url] 

370 

371 logger.info("Disconnecting tunnel: {}".format(tunnel.public_url)) 

372 

373 api_request("{}{}".format(api_url, tunnel.uri), method="DELETE", 

374 timeout=pyngrok_config.request_timeout) 

375 

376 _current_tunnels.pop(public_url, None) 

377 

378 

379def get_tunnels(pyngrok_config=None): 

380 """ 

381 Get a list of active ``ngrok`` tunnels for the given config's ``ngrok_path``. 

382 

383 If ``ngrok`` is not installed at :class:`~pyngrok.conf.PyngrokConfig`'s ``ngrok_path``, calling this method 

384 will first download and install ``ngrok``. 

385 

386 If ``ngrok`` is not running, calling this method will first start a process with 

387 :class:`~pyngrok.conf.PyngrokConfig`. 

388 

389 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

390 overriding :func:`~pyngrok.conf.get_default()`. 

391 :type pyngrok_config: PyngrokConfig, optional 

392 :return: The active ``ngrok`` tunnels. 

393 :rtype: list[NgrokTunnel] 

394 """ 

395 if pyngrok_config is None: 

396 pyngrok_config = conf.get_default() 

397 

398 api_url = get_ngrok_process(pyngrok_config).api_url 

399 

400 _current_tunnels.clear() 

401 for tunnel in api_request("{}/api/tunnels".format(api_url), method="GET", 

402 timeout=pyngrok_config.request_timeout)["tunnels"]: 

403 ngrok_tunnel = NgrokTunnel(tunnel, pyngrok_config, api_url) 

404 _apply_cloud_edge_to_tunnel(ngrok_tunnel, pyngrok_config) 

405 _current_tunnels[ngrok_tunnel.public_url] = ngrok_tunnel 

406 

407 return list(_current_tunnels.values()) 

408 

409 

410def kill(pyngrok_config=None): 

411 """ 

412 Terminate the ``ngrok`` processes, if running, for the given config's ``ngrok_path``. This method will not 

413 block, it will just issue a kill request. 

414 

415 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

416 overriding :func:`~pyngrok.conf.get_default()`. 

417 :type pyngrok_config: PyngrokConfig, optional 

418 """ 

419 if pyngrok_config is None: 

420 pyngrok_config = conf.get_default() 

421 

422 process.kill_process(pyngrok_config.ngrok_path) 

423 

424 _current_tunnels.clear() 

425 

426 

427def get_version(pyngrok_config=None): 

428 """ 

429 Get a tuple with the ``ngrok`` and ``pyngrok`` versions. 

430 

431 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

432 overriding :func:`~pyngrok.conf.get_default()`. 

433 :type pyngrok_config: PyngrokConfig, optional 

434 :return: A tuple of ``(ngrok_version, pyngrok_version)``. 

435 :rtype: tuple 

436 """ 

437 if pyngrok_config is None: 

438 pyngrok_config = conf.get_default() 

439 

440 ngrok_version = process.capture_run_process(pyngrok_config.ngrok_path, ["--version"]).split("version ")[1] 

441 

442 return ngrok_version, __version__ 

443 

444 

445def update(pyngrok_config=None): 

446 """ 

447 Update ``ngrok`` for the given config's ``ngrok_path``, if an update is available. 

448 

449 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

450 overriding :func:`~pyngrok.conf.get_default()`. 

451 :type pyngrok_config: PyngrokConfig, optional 

452 :return: The result from the ``ngrok`` update. 

453 :rtype: str 

454 """ 

455 if pyngrok_config is None: 

456 pyngrok_config = conf.get_default() 

457 

458 return process.capture_run_process(pyngrok_config.ngrok_path, ["update"]) 

459 

460 

461def api_request(url, method="GET", data=None, params=None, timeout=4, auth=None): 

462 """ 

463 Invoke an API request to the given URL, returning JSON data from the response. 

464 

465 One use for this method is making requests to ``ngrok`` tunnels: 

466 

467 .. code-block:: python 

468 

469 from pyngrok import ngrok 

470 

471 public_url = ngrok.connect() 

472 response = ngrok.api_request("{}/some-route".format(public_url), 

473 method="POST", data={"foo": "bar"}) 

474 

475 Another is making requests to the ``ngrok`` API itself: 

476 

477 .. code-block:: python 

478 

479 from pyngrok import ngrok 

480 

481 api_url = ngrok.get_ngrok_process().api_url 

482 response = ngrok.api_request("{}/api/requests/http".format(api_url), 

483 params={"tunnel_name": "foo"}) 

484 

485 :param url: The request URL. 

486 :type url: str 

487 :param method: The HTTP method. 

488 :type method: str, optional 

489 :param data: The request body. 

490 :type data: dict, optional 

491 :param params: The URL parameters. 

492 :type params: dict, optional 

493 :param timeout: The request timeout, in seconds. 

494 :type timeout: float, optional 

495 :param auth: Set as Bearer for an Authorization header. 

496 :type auth: str, optional 

497 :return: The response from the request. 

498 :rtype: dict 

499 """ 

500 if params is None: 

501 params = [] 

502 

503 if not url.lower().startswith("http"): 

504 raise PyngrokSecurityError("URL must start with \"http\": {}".format(url)) 

505 

506 data = json.dumps(data).encode("utf-8") if data else None 

507 

508 if params: 

509 url += "?{}".format(urlencode([(x, params[x]) for x in params])) 

510 

511 request = Request(url, method=method.upper()) 

512 request.add_header("Content-Type", "application/json") 

513 if auth: 

514 request.add_header("Ngrok-Version", "2") 

515 request.add_header("Authorization", "Bearer {}".format(auth)) 

516 

517 logger.debug("Making {} request to {} with data: {}".format(method, url, data)) 

518 

519 try: 

520 response = urlopen(request, data, timeout) 

521 response_data = response.read().decode("utf-8") 

522 

523 status_code = response.getcode() 

524 logger.debug("Response {}: {}".format(status_code, response_data.strip())) 

525 

526 if str(status_code)[0] != "2": 

527 raise PyngrokNgrokHTTPError("ngrok client API returned {}: {}".format(status_code, response_data), url, 

528 status_code, None, request.headers, response_data) 

529 elif status_code == HTTPStatus.NO_CONTENT: 

530 return None 

531 

532 return json.loads(response_data) 

533 except socket.timeout: 

534 raise PyngrokNgrokURLError("ngrok client exception, URLError: timed out", "timed out") 

535 except HTTPError as e: 

536 response_data = e.read().decode("utf-8") 

537 

538 status_code = e.getcode() 

539 logger.debug("Response {}: {}".format(status_code, response_data.strip())) 

540 

541 raise PyngrokNgrokHTTPError("ngrok client exception, API returned {}: {}".format(status_code, response_data), 

542 e.url, 

543 status_code, e.msg, e.hdrs, response_data) 

544 except URLError as e: 

545 raise PyngrokNgrokURLError("ngrok client exception, URLError: {}".format(e.reason), e.reason) 

546 

547 

548def run(args=None, pyngrok_config=None): 

549 """ 

550 Ensure ``ngrok`` is installed at the default path, then call :func:`~pyngrok.process.run_process`. 

551 

552 This method is meant for interacting with ``ngrok`` from the command line and is not necessarily 

553 compatible with non-blocking API methods. For that, use :mod:`~pyngrok.ngrok`'s interface methods (like 

554 :func:`~pyngrok.ngrok.connect`), or use :func:`~pyngrok.process.get_process`. 

555 

556 :param args: Arguments to be passed to the ``ngrok`` process. 

557 :type args: list[str], optional 

558 :param pyngrok_config: A ``pyngrok`` configuration to use when interacting with the ``ngrok`` binary, 

559 overriding :func:`~pyngrok.conf.get_default()`. 

560 :type pyngrok_config: PyngrokConfig, optional 

561 """ 

562 if args is None: 

563 args = [] 

564 if pyngrok_config is None: 

565 pyngrok_config = conf.get_default() 

566 

567 install_ngrok(pyngrok_config) 

568 

569 process.run_process(pyngrok_config.ngrok_path, args) 

570 

571 

572def main(): 

573 """ 

574 Entry point for the package's ``console_scripts``. This initializes a call from the command 

575 line and invokes :func:`~pyngrok.ngrok.run`. 

576 

577 This method is meant for interacting with ``ngrok`` from the command line and is not necessarily 

578 compatible with non-blocking API methods. For that, use :mod:`~pyngrok.ngrok`'s interface methods (like 

579 :func:`~pyngrok.ngrok.connect`), or use :func:`~pyngrok.process.get_process`. 

580 """ 

581 run(sys.argv[1:]) 

582 

583 if len(sys.argv) == 1 or len(sys.argv) == 2 and sys.argv[1].lstrip("-").lstrip("-") == "help": 

584 print("\nPYNGROK VERSION:\n {}".format(__version__)) 

585 elif len(sys.argv) == 2 and sys.argv[1].lstrip("-").lstrip("-") in ["v", "version"]: 

586 print("pyngrok version {}".format(__version__)) 

587 

588 

589if __name__ == "__main__": 

590 main()