Coverage for pyngrok/ngrok.py: 87.91%

215 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-19 14:58 +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 

14from pyngrok.installer import get_default_config 

15 

16__author__ = "Alex Laird" 

17__copyright__ = "Copyright 2023, Alex Laird" 

18__version__ = "6.1.2" 

19 

20logger = logging.getLogger(__name__) 

21 

22_current_tunnels = {} 

23 

24 

25class NgrokTunnel: 

26 """ 

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

28 

29 :var data: The original tunnel data. 

30 :vartype data: dict 

31 :var name: The name of the tunnel. 

32 :vartype name: str 

33 :var proto: The protocol of the tunnel. 

34 :vartype proto: str 

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

36 :vartype uri: str 

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

38 :vartype public_url: str 

39 :var config: The config for the tunnel. 

40 :vartype config: dict 

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

42 :vartype metrics: dict 

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

44 :vartype pyngrok_config: PyngrokConfig 

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

46 :vartype api_url: str 

47 """ 

48 

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

50 self.data = data 

51 

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

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

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

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

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

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

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

59 

60 self.pyngrok_config = pyngrok_config 

61 self.api_url = api_url 

62 

63 def __repr__(self): 

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

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

66 

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

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

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

70 

71 def refresh_metrics(self): 

72 """ 

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

74 """ 

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

76 

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

78 timeout=self.pyngrok_config.request_timeout) 

79 

80 if "metrics" not in data: 

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

82 

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

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

85 

86 

87def install_ngrok(pyngrok_config=None): 

88 """ 

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

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

91 

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

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

94 :type pyngrok_config: PyngrokConfig, optional 

95 """ 

96 if pyngrok_config is None: 

97 pyngrok_config = conf.get_default() 

98 

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

100 installer.install_ngrok(pyngrok_config.ngrok_path, ngrok_version=pyngrok_config.ngrok_version) 

101 

102 config_path = conf.get_config_path(pyngrok_config) 

103 

104 # Install the config to the requested path 

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

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

107 

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

109 if conf.DEFAULT_NGROK_CONFIG_PATH != config_path and \ 

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

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

112 

113 

114def set_auth_token(token, pyngrok_config=None): 

115 """ 

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

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

118 

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

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

121 

122 :param token: The auth token to set. 

123 :type token: str 

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

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

126 :type pyngrok_config: PyngrokConfig, optional 

127 """ 

128 if pyngrok_config is None: 

129 pyngrok_config = conf.get_default() 

130 

131 install_ngrok(pyngrok_config) 

132 

133 process.set_auth_token(pyngrok_config, token) 

134 

135 

136def get_ngrok_process(pyngrok_config=None): 

137 """ 

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

139 

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

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

142 

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

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

145 

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

147 installing and starting it. 

148 

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

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

151 :type pyngrok_config: PyngrokConfig, optional 

152 :return: The ``ngrok`` process. 

153 :rtype: NgrokProcess 

154 """ 

155 if pyngrok_config is None: 

156 pyngrok_config = conf.get_default() 

157 

158 install_ngrok(pyngrok_config) 

159 

160 return process.get_process(pyngrok_config) 

161 

162 

163def _apply_cloud_edge_to_tunnel(tunnel, pyngrok_config): 

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

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

166 auth=pyngrok_config.api_key) 

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

168 raise PyngrokError( 

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

170 

171 edge = tunnel_response["labels"]["edge"] 

172 if edge.startswith("edghts_"): 

173 edges_prefix = "https" 

174 elif edge.startswith("edgtcp"): 

175 edges_prefix = "tcp" 

176 elif edge.startswith("edgtls"): 

177 edges_prefix = "tls" 

178 else: 

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

180 

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

182 auth=pyngrok_config.api_key) 

183 

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

185 raise PyngrokError( 

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

187 edge)) 

188 

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

190 tunnel.proto = edges_prefix 

191 

192 

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

194 """ 

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

196 the connected tunnel. 

197 

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

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

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

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

202 override properties from the loaded tunnel definition. 

203 

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

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

206 

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

208 set ``ngrok_version`` to "v2" in :class:`~pyngrok.conf.PyngrokConfig`: 

209 

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

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

212 

213 .. note:: 

214 

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

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

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

218 

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

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

221 :type addr: str, optional 

222 :param proto: A valid `tunnel protocol 

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

224 :type proto: str, optional 

225 :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>`_ 

226 to be used. 

227 :type name: str, optional 

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

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

230 :type pyngrok_config: PyngrokConfig, optional 

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

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

233 :type options: dict, optional 

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

235 :rtype: NgrokTunnel 

236 """ 

237 if "labels" in options: 

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

239 

240 if pyngrok_config is None: 

241 pyngrok_config = conf.get_default() 

242 

243 config_path = conf.get_config_path(pyngrok_config) 

244 

245 if os.path.exists(config_path): 

246 config = installer.get_ngrok_config(config_path, ngrok_version=pyngrok_config.ngrok_version) 

247 else: 

248 config = get_default_config(pyngrok_config.ngrok_version) 

249 

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

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

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

253 name = "pyngrok-default" 

254 

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

256 if name and name in tunnel_definitions: 

257 tunnel_definition = tunnel_definitions[name] 

258 

259 if "labels" in tunnel_definition and "bind_tls" in options: 

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

261 

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

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

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

265 tunnel_definition.update(options) 

266 options = tunnel_definition 

267 

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

269 raise PyngrokError( 

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

271 

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

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

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

275 proto = "http" 

276 

277 if not name: 

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

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

280 else: 

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

282 

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

284 

285 config = { 

286 "name": name, 

287 "addr": addr 

288 } 

289 options.update(config) 

290 

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

292 if "labels" not in options: 

293 options["proto"] = proto 

294 

295 # Upgrade legacy parameters, if present 

296 if pyngrok_config.ngrok_version == "v3": 

297 if "bind_tls" in options: 

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

299 options["schemes"] = ["https"] 

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

301 options["schemes"] = ["http"] 

302 else: 

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

304 

305 options.pop("bind_tls") 

306 

307 if "auth" in options: 

308 auth = options.get("auth") 

309 if isinstance(auth, list): 

310 options["basic_auth"] = auth 

311 else: 

312 options["basic_auth"] = [auth] 

313 

314 options.pop("auth") 

315 

316 api_url = get_ngrok_process(pyngrok_config).api_url 

317 

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

319 

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

321 timeout=pyngrok_config.request_timeout), 

322 pyngrok_config, api_url) 

323 

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

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

326 timeout=pyngrok_config.request_timeout), 

327 pyngrok_config, api_url) 

328 

329 _apply_cloud_edge_to_tunnel(tunnel, pyngrok_config) 

330 

331 _current_tunnels[tunnel.public_url] = tunnel 

332 

333 return tunnel 

334 

335 

336def disconnect(public_url, pyngrok_config=None): 

337 """ 

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

339 

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

341 :type public_url: str 

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

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

344 :type pyngrok_config: PyngrokConfig, optional 

345 """ 

346 if pyngrok_config is None: 

347 pyngrok_config = conf.get_default() 

348 

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

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

351 return 

352 

353 api_url = get_ngrok_process(pyngrok_config).api_url 

354 

355 if public_url not in _current_tunnels: 

356 get_tunnels(pyngrok_config) 

357 

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

359 if public_url not in _current_tunnels: 

360 return 

361 

362 tunnel = _current_tunnels[public_url] 

363 

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

365 

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

367 timeout=pyngrok_config.request_timeout) 

368 

369 _current_tunnels.pop(public_url, None) 

370 

371 

372def get_tunnels(pyngrok_config=None): 

373 """ 

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

375 

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

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

378 

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

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

381 

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

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

384 :type pyngrok_config: PyngrokConfig, optional 

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

386 :rtype: list[NgrokTunnel] 

387 """ 

388 if pyngrok_config is None: 

389 pyngrok_config = conf.get_default() 

390 

391 api_url = get_ngrok_process(pyngrok_config).api_url 

392 

393 _current_tunnels.clear() 

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

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

396 ngrok_tunnel = NgrokTunnel(tunnel, pyngrok_config, api_url) 

397 _apply_cloud_edge_to_tunnel(ngrok_tunnel, pyngrok_config) 

398 _current_tunnels[ngrok_tunnel.public_url] = ngrok_tunnel 

399 

400 return list(_current_tunnels.values()) 

401 

402 

403def kill(pyngrok_config=None): 

404 """ 

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

406 block, it will just issue a kill request. 

407 

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

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

410 :type pyngrok_config: PyngrokConfig, optional 

411 """ 

412 if pyngrok_config is None: 

413 pyngrok_config = conf.get_default() 

414 

415 process.kill_process(pyngrok_config.ngrok_path) 

416 

417 _current_tunnels.clear() 

418 

419 

420def get_version(pyngrok_config=None): 

421 """ 

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

423 

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

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

426 :type pyngrok_config: PyngrokConfig, optional 

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

428 :rtype: tuple 

429 """ 

430 if pyngrok_config is None: 

431 pyngrok_config = conf.get_default() 

432 

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

434 

435 return ngrok_version, __version__ 

436 

437 

438def update(pyngrok_config=None): 

439 """ 

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

441 

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

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

444 :type pyngrok_config: PyngrokConfig, optional 

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

446 :rtype: str 

447 """ 

448 if pyngrok_config is None: 

449 pyngrok_config = conf.get_default() 

450 

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

452 

453 

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

455 """ 

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

457 

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

459 

460 .. code-block:: python 

461 

462 from pyngrok import ngrok 

463 

464 public_url = ngrok.connect() 

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

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

467 

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

469 

470 .. code-block:: python 

471 

472 from pyngrok import ngrok 

473 

474 api_url = ngrok.get_ngrok_process().api_url 

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

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

477 

478 :param url: The request URL. 

479 :type url: str 

480 :param method: The HTTP method. 

481 :type method: str, optional 

482 :param data: The request body. 

483 :type data: dict, optional 

484 :param params: The URL parameters. 

485 :type params: dict, optional 

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

487 :type timeout: float, optional 

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

489 :type auth: str, optional 

490 :return: The response from the request. 

491 :rtype: dict 

492 """ 

493 if params is None: 

494 params = {} 

495 

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

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

498 

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

500 

501 if params: 

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

503 

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

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

506 if auth: 

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

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

509 

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

511 

512 try: 

513 response = urlopen(request, encoded_data, timeout) 

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

515 

516 status_code = response.getcode() 

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

518 

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

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

521 status_code, None, request.headers, response_data) 

522 elif status_code == HTTPStatus.NO_CONTENT: 

523 return {} 

524 

525 return json.loads(response_data) 

526 except socket.timeout: 

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

528 except HTTPError as e: 

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

530 

531 status_code = e.getcode() 

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

533 

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

535 e.url, 

536 status_code, e.reason, e.headers, response_data) 

537 except URLError as e: 

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

539 

540 

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

542 """ 

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

544 

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

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

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

548 

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

550 :type args: list[str], optional 

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

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

553 :type pyngrok_config: PyngrokConfig, optional 

554 """ 

555 if args is None: 

556 args = [] 

557 if pyngrok_config is None: 

558 pyngrok_config = conf.get_default() 

559 

560 install_ngrok(pyngrok_config) 

561 

562 process.run_process(pyngrok_config.ngrok_path, args) 

563 

564 

565def main(): 

566 """ 

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

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

569 

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

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

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

573 """ 

574 run(sys.argv[1:]) 

575 

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

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

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

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

580 

581 

582if __name__ == "__main__": 

583 main()