Coverage for pyngrok/ngrok.py: 87.39%

222 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-14 22:59 +0000

1import json 

2import logging 

3import os 

4import socket 

5import sys 

6import uuid 

7from http import HTTPStatus 

8from typing import Optional, Any, Dict, List, Tuple 

9from urllib.error import HTTPError, URLError 

10from urllib.parse import urlencode 

11from urllib.request import urlopen, Request 

12 

13from pyngrok import process, conf, installer 

14from pyngrok.conf import PyngrokConfig 

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

16from pyngrok.installer import get_default_config 

17from pyngrok.process import NgrokProcess 

18 

19__author__ = "Alex Laird" 

20__copyright__ = "Copyright 2023, Alex Laird" 

21__version__ = "7.0.1" 

22 

23logger = logging.getLogger(__name__) 

24 

25 

26class NgrokTunnel: 

27 """ 

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

29 """ 

30 

31 def __init__(self, 

32 data: Dict[str, Any], 

33 pyngrok_config: PyngrokConfig, 

34 api_url: Optional[str]) -> None: 

35 #: The original tunnel data. 

36 self.data: Dict[str, Any] = data 

37 #: The ``pyngrok`` configuration to use when interacting with the ``ngrok``. 

38 self.pyngrok_config: PyngrokConfig = pyngrok_config 

39 #: The API URL for the ``ngrok`` web interface. 

40 self.api_url: Optional[str] = api_url 

41 

42 #: The ID of the tunnel. 

43 self.id: Optional[str] = data.get("ID", None) 

44 #: The name of the tunnel. 

45 self.name: Optional[str] = data.get("name") 

46 #: The protocol of the tunnel. 

47 self.proto: Optional[str] = data.get("proto") 

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

49 self.uri: Optional[str] = data.get("uri") 

50 #: The public ``ngrok`` URL. 

51 self.public_url: Optional[str] = data.get("public_url") 

52 #: The config for the tunnel. 

53 self.config: Dict[str, Any] = data.get("config", {}) 

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

55 self.metrics: Dict[str, Any] = data.get("metrics", {}) 

56 

57 def __repr__(self) -> str: 

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

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

60 

61 def __str__(self) -> str: # pragma: no cover 

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

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

64 

65 def refresh_metrics(self) -> None: 

66 """ 

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

68 """ 

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

70 

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

72 timeout=self.pyngrok_config.request_timeout) 

73 

74 if "metrics" not in data: 

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

76 

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

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

79 

80 

81_current_tunnels: Dict[str, NgrokTunnel] = {} 

82 

83 

84def install_ngrok(pyngrok_config: Optional[PyngrokConfig] = None) -> None: 

85 """ 

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

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

88 

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

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

91 """ 

92 if pyngrok_config is None: 

93 pyngrok_config = conf.get_default() 

94 

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

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

97 

98 config_path = conf.get_config_path(pyngrok_config) 

99 

100 # Install the config to the requested path 

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

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

103 

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

105 if conf.DEFAULT_NGROK_CONFIG_PATH != config_path and \ 

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

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

108 

109 

110def set_auth_token(token: str, 

111 pyngrok_config: Optional[PyngrokConfig] = None) -> None: 

112 """ 

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

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

115 

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

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

118 

119 :param token: The auth token to set. 

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

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

122 """ 

123 if pyngrok_config is None: 

124 pyngrok_config = conf.get_default() 

125 

126 install_ngrok(pyngrok_config) 

127 

128 process.set_auth_token(pyngrok_config, token) 

129 

130 

131def get_ngrok_process(pyngrok_config: Optional[PyngrokConfig] = None) -> NgrokProcess: 

132 """ 

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

134 

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

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

137 

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

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

140 

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

142 installing and starting it. 

143 

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

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

146 :return: The ``ngrok`` process. 

147 """ 

148 if pyngrok_config is None: 

149 pyngrok_config = conf.get_default() 

150 

151 install_ngrok(pyngrok_config) 

152 

153 return process.get_process(pyngrok_config) 

154 

155 

156def _apply_cloud_edge_to_tunnel(tunnel: NgrokTunnel, 

157 pyngrok_config: PyngrokConfig) -> None: 

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

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

160 auth=pyngrok_config.api_key) 

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

162 raise PyngrokError( 

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

164 

165 edge = tunnel_response["labels"]["edge"] 

166 if edge.startswith("edghts_"): 

167 edges_prefix = "https" 

168 elif edge.startswith("edgtcp"): 

169 edges_prefix = "tcp" 

170 elif edge.startswith("edgtls"): 

171 edges_prefix = "tls" 

172 else: 

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

174 

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

176 auth=pyngrok_config.api_key) 

177 

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

179 raise PyngrokError( 

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

181 edge)) 

182 

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

184 tunnel.proto = edges_prefix 

185 

186 

187# When Python <3.9 support is dropped, addr type can be changed to Optional[str|int] 

188def connect(addr: Optional[str] = None, 

189 proto: Optional[str] = None, 

190 name: Optional[str] = None, 

191 pyngrok_config: Optional[PyngrokConfig] = None, 

192 **options: Any) -> NgrokTunnel: 

193 """ 

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

195 the connected tunnel. 

196 

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

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

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

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

201 override properties from the loaded tunnel definition. 

202 

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

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

205 

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

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

208 

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

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

211 

212 .. note:: 

213 

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

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

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

217 

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

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

220 :param proto: A valid `tunnel protocol 

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

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

223 to be used. 

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

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

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

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

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

229 """ 

230 if "labels" in options: 

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

232 

233 if pyngrok_config is None: 

234 pyngrok_config = conf.get_default() 

235 

236 config_path = conf.get_config_path(pyngrok_config) 

237 

238 if os.path.exists(config_path): 

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

240 else: 

241 config = get_default_config(pyngrok_config.ngrok_version) 

242 

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

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

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

246 name = "pyngrok-default" 

247 

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

249 if name and name in tunnel_definitions: 

250 tunnel_definition = tunnel_definitions[name] 

251 

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

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

254 

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

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

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

258 tunnel_definition.update(options) 

259 options = tunnel_definition 

260 

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

262 raise PyngrokError( 

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

264 

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

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

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

268 proto = "http" 

269 

270 if not name: 

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

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

273 else: 

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

275 

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

277 

278 config = { 

279 "name": name, 

280 "addr": addr 

281 } 

282 options.update(config) 

283 

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

285 if "labels" not in options: 

286 options["proto"] = proto 

287 

288 # Upgrade legacy parameters, if present 

289 if pyngrok_config.ngrok_version == "v3": 

290 if "bind_tls" in options: 

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

292 options["schemes"] = ["https"] 

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

294 options["schemes"] = ["http"] 

295 else: 

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

297 

298 options.pop("bind_tls") 

299 

300 if "auth" in options: 

301 auth = options.get("auth") 

302 if isinstance(auth, list): 

303 options["basic_auth"] = auth 

304 else: 

305 options["basic_auth"] = [auth] 

306 

307 options.pop("auth") 

308 

309 api_url = get_ngrok_process(pyngrok_config).api_url 

310 

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

312 

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

314 timeout=pyngrok_config.request_timeout), 

315 pyngrok_config, api_url) 

316 

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

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

319 timeout=pyngrok_config.request_timeout), 

320 pyngrok_config, api_url) 

321 

322 _apply_cloud_edge_to_tunnel(tunnel, pyngrok_config) 

323 

324 if tunnel.public_url is None: 

325 raise PyngrokError( 

326 "\"public_url\" was not populated for tunnel {}, but is required for pyngrok to function.".format( 

327 tunnel)) 

328 

329 _current_tunnels[tunnel.public_url] = tunnel 

330 

331 return tunnel 

332 

333 

334def disconnect(public_url: str, 

335 pyngrok_config: Optional[PyngrokConfig] = None) -> None: 

336 """ 

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

338 

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

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

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

342 """ 

343 if pyngrok_config is None: 

344 pyngrok_config = conf.get_default() 

345 

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

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

348 return 

349 

350 api_url = get_ngrok_process(pyngrok_config).api_url 

351 

352 if public_url not in _current_tunnels: 

353 get_tunnels(pyngrok_config) 

354 

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

356 if public_url not in _current_tunnels: 

357 return 

358 

359 tunnel = _current_tunnels[public_url] 

360 

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

362 

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

364 timeout=pyngrok_config.request_timeout) 

365 

366 _current_tunnels.pop(public_url, None) 

367 

368 

369def get_tunnels(pyngrok_config: Optional[PyngrokConfig] = None) -> List[NgrokTunnel]: 

370 """ 

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

372 

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

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

375 

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

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

378 

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

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

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

382 """ 

383 if pyngrok_config is None: 

384 pyngrok_config = conf.get_default() 

385 

386 api_url = get_ngrok_process(pyngrok_config).api_url 

387 

388 _current_tunnels.clear() 

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

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

391 ngrok_tunnel = NgrokTunnel(tunnel, pyngrok_config, api_url) 

392 _apply_cloud_edge_to_tunnel(ngrok_tunnel, pyngrok_config) 

393 

394 if ngrok_tunnel.public_url is None: 

395 raise PyngrokError( 

396 "\"public_url\" was not populated for tunnel {}, but is required for pyngrok to function.".format( 

397 ngrok_tunnel)) 

398 

399 _current_tunnels[ngrok_tunnel.public_url] = ngrok_tunnel 

400 

401 return list(_current_tunnels.values()) 

402 

403 

404def kill(pyngrok_config: Optional[PyngrokConfig] = None) -> None: 

405 """ 

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

407 block, it will just issue a kill request. 

408 

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

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

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: Optional[PyngrokConfig] = None) -> Tuple[str, str]: 

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 :return: A tuple of ``(ngrok_version, pyngrok_version)``. 

427 """ 

428 if pyngrok_config is None: 

429 pyngrok_config = conf.get_default() 

430 

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

432 

433 return ngrok_version, __version__ 

434 

435 

436def update(pyngrok_config: Optional[PyngrokConfig] = None) -> str: 

437 """ 

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

439 

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

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

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

443 """ 

444 if pyngrok_config is None: 

445 pyngrok_config = conf.get_default() 

446 

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

448 

449 

450def api_request(url: str, 

451 method: str = "GET", 

452 data: Optional[Dict[str, Any]] = None, 

453 params: Optional[Dict[str, Any]] = None, 

454 timeout: float = 4, 

455 auth: Optional[str] = None) -> Dict[str, Any]: 

456 """ 

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

458 

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

460 

461 .. code-block:: python 

462 

463 from pyngrok import ngrok 

464 

465 public_url = ngrok.connect() 

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

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

468 

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

470 

471 .. code-block:: python 

472 

473 from pyngrok import ngrok 

474 

475 api_url = ngrok.get_ngrok_process().api_url 

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

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

478 

479 :param url: The request URL. 

480 :param method: The HTTP method. 

481 :param data: The request body. 

482 :param params: The URL parameters. 

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

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

485 :return: The response from the request. 

486 """ 

487 if params is None: 

488 params = {} 

489 

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

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

492 

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

494 

495 if params: 

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

497 

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

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

500 if auth: 

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

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

503 

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

505 

506 try: 

507 response = urlopen(request, encoded_data, timeout) 

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

509 

510 status_code = response.getcode() 

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

512 

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

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

515 status_code, None, request.headers, response_data) 

516 elif status_code == HTTPStatus.NO_CONTENT: 

517 return {} 

518 

519 return json.loads(response_data) 

520 except socket.timeout: 

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

522 except HTTPError as e: 

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

524 

525 status_code = e.getcode() 

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

527 

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

529 e.url, 

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

531 except URLError as e: 

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

533 

534 

535def run(args: Optional[List[str]] = None, 

536 pyngrok_config: Optional[PyngrokConfig] = None) -> None: 

537 """ 

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

539 

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

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

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

543 

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

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

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

547 """ 

548 if args is None: 

549 args = [] 

550 if pyngrok_config is None: 

551 pyngrok_config = conf.get_default() 

552 

553 install_ngrok(pyngrok_config) 

554 

555 process.run_process(pyngrok_config.ngrok_path, args) 

556 

557 

558def main() -> None: 

559 """ 

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

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

562 

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

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

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

566 """ 

567 run(sys.argv[1:]) 

568 

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

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

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

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

573 

574 

575if __name__ == "__main__": 

576 main()