Skip to content

Commit

Permalink
RC for 4.1.14 (#70)
Browse files Browse the repository at this point in the history
* Added NgrokTunnel.refresh_metrics().

* Error message cleanup.

* Python 2 compatibility issue in test.

* Changelog improvements.

* Version bump.

* Clarifying mock import.

* Version bump, adding pinned version to changelog.

* Fixing typos in docs.

* Fixing broken test.

* Fix OS X Python build in Travis.

* Updating Python 2.7 deprecation message.

* Fixing changelog syntax.

* Doc updates.
  • Loading branch information
alexdlaird committed Oct 11, 2020
1 parent 89a5f80 commit 3bed665
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 46 deletions.
4 changes: 1 addition & 3 deletions .travis.yml
Expand Up @@ -47,9 +47,7 @@ jobs:
language: shell
osx_image: "xcode11.2"
install:
- python3 -m pip install virtualenv
- python3 -m virtualenv venv
- make install
- export PYTHON_BIN=python3
- name: "Windows, Python: 3.7"
os: windows
language: shell
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Expand Up @@ -3,8 +3,11 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/alexdlaird/pyngrok/compare/4.1.13...HEAD)
## [Unreleased](https://github.com/alexdlaird/pyngrok/compare/4.1.14...HEAD)

## [4.1.14](https://github.com/alexdlaird/pyngrok/compare/4.1.13...4.1.14) - 2020-10-11
### Added
- `refresh_metrics()` to [NgrokTunnel](https://pyngrok.readthedocs.io/en/4.1.14/api.html#pyngrok.ngrok.NgrokTunnel.refresh_metrics).
- Documentation improvements.

## [4.1.13](https://github.com/alexdlaird/pyngrok/compare/4.1.12...4.1.13) - 2020-10-02
Expand Down Expand Up @@ -105,7 +108,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [4.0.0](https://github.com/alexdlaird/pyngrok/compare/3.1.1...4.0.0) - 2020-06-06
### Added
- `PyngrokConfig`, which contains all of `pyngrok`'s configuration for interacting with the `ngrok` binary rather than passing these values around in an ever-growing list of kwargs. It is documented [here](https://pyngrok.readthedocs.io/en/4.0.0/api.html#pyngrok.conf.PyngrokConfig).
- `PyngrokConfig`, which contains all of `pyngrok`'s configuration for interacting with the `ngrok` binary rather than passing these values around in an ever-growing list of `kwargs`. It is documented [here](https://pyngrok.readthedocs.io/en/4.0.0/api.html#pyngrok.conf.PyngrokConfig).
- `log_event_callback` is a new configuration parameter in `PyngrokConfig`, a callback that will be invoked each time a `ngrok` log is emitted.
- `monitor_thread` is a new configuration parameter in `PyngrokConfig` which determines whether `ngrok` should continue to be monitored (for logs, etc.) after it has finished starting. Defaults to `True`.
- `startup_timeout` is a new configuration parameter in `PyngrokConfig`.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -60,6 +60,11 @@ ngrok http 80

For details on how to fully leverage `ngrok` from the command line, see [ngrok's official documentation](https://ngrok.com/docs).

### Python 2.7

The last version of `pyngrok` that supports Python 2.7 is 4.1.x, so we need to pin `pyngrok>=4.1,<4.2` if we still want
to use `pyngrok` with this version of Python.

## Documentation

For more advanced usage, `pyngrok`'s official documentation is available at [http://pyngrok.readthedocs.io](http://pyngrok.readthedocs.io).
Expand Down
6 changes: 6 additions & 0 deletions docs/index.rst
Expand Up @@ -359,6 +359,12 @@ available on the command line.
For details on how to fully leverage ``ngrok`` from the command line, see `ngrok's official documentation <https://ngrok.com/docs>`_.

Python 2.7
==========

The last version of ``pyngrok`` that supports Python 2.7 is 4.1.x, so we need to pin ``pyngrok>=4.1,<4.2`` if we still
want to use ``pyngrok`` with this version of Python.

Dive Deeper
===========

Expand Down
17 changes: 9 additions & 8 deletions docs/integrations.rst
Expand Up @@ -43,7 +43,7 @@ same place.
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port)
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, port))
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))
# Update any base URLs or webhooks to use the public ngrok URL
app.config["BASE_URL"] = public_url
Expand Down Expand Up @@ -106,8 +106,8 @@ to do this is one of our ``apps.py`` by `extending AppConfig <https://docs.djang
port = addrport.port if addrport.netloc and addrport.port else 8000
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port).rstrip("/")
print("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, port))
public_url = ngrok.connect(port)
print("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))
# Update any base URLs or webhooks to use the public ngrok URL
settings.BASE_URL = public_url
Expand Down Expand Up @@ -169,7 +169,7 @@ we should add a variable that let's us configure from an environment variable wh
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port)
logger.info("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, port))
logger.info("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))
# Update any base URLs or webhooks to use the public ngrok URL
settings.BASE_URL = public_url
Expand Down Expand Up @@ -242,10 +242,11 @@ assumes we have also added ``!pip install flask`` to our dependency code block.
os.environ["FLASK_ENV"] = "development"
app = Flask(__name__)
port = 5000
# Open a ngrok tunnel to the HTTP server
public_url = ngrok.connect(5000)
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, 5000))
public_url = ngrok.connect(port)
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))
# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = public_url
Expand Down Expand Up @@ -392,7 +393,7 @@ server. We can use ``pyngrok`` to expose it to the web via a tunnel, as show in
httpd = HTTPServer(server_address, BaseHTTPRequestHandler)
public_url = ngrok.connect(port)
print("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, port))
print("ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))
try:
# Block until CTRL-C or some other terminating event
Expand Down Expand Up @@ -440,7 +441,7 @@ Now create ``server.py`` with the following code:
# Open a ngrok tunnel to the socket
public_url = ngrok.connect(port, "tcp", options={"remote_addr": "{}:{}".format(host, port)})
print("ngrok tunnel \"{}\" -> \"tcp://127.0.0.1:{}/\"".format(public_url, port))
print("ngrok tunnel \"{}\" -> \"tcp://127.0.0.1:{}\"".format(public_url, port))
while True:
connection = None
Expand Down
6 changes: 3 additions & 3 deletions pyngrok/installer.py
Expand Up @@ -71,7 +71,7 @@ def install_ngrok(ngrok_path, **kwargs):
:param ngrok_path: The path to where the ``ngrok`` binary will be downloaded.
:type ngrok_path: str
:param kwargs: Remaining kwargs will be passed to :func:`_download_file`.
:param kwargs: Remaining ``kwargs`` will be passed to :func:`_download_file`.
:type kwargs: dict, optional
"""
logger.debug(
Expand Down Expand Up @@ -127,7 +127,7 @@ def _install_ngrok_zip(ngrok_path, zip_path):

def install_default_config(config_path, data=None):
"""
Install the default ``ngrok`` config. If one is not already present, created one. Before saving
Install the default ``ngrok`` config. If one is not already present, create one. Before saving
new values to the default config, validate that they are compatible with ``pyngrok``.
:param config_path: The path to where the ``ngrok`` config should be installed.
Expand Down Expand Up @@ -180,7 +180,7 @@ def _download_file(url, retries=0, **kwargs):
:type url: str
:param retries: The number of retries to attempt, if download fails.
:type retries: int, optional
:param kwargs: Remaining kwargs will be passed to :py:func:`urllib.request.urlopen`.
:param kwargs: Remaining ``kwargs`` will be passed to :py:func:`urllib.request.urlopen`.
:type kwargs: dict, optional
:return: The path to the downloaded temporary file.
:rtype: str
Expand Down
48 changes: 36 additions & 12 deletions pyngrok/ngrok.py
Expand Up @@ -8,7 +8,7 @@
from future.standard_library import install_aliases

from pyngrok import process, conf
from pyngrok.exception import PyngrokNgrokHTTPError, PyngrokNgrokURLError, PyngrokSecurityError
from pyngrok.exception import PyngrokNgrokHTTPError, PyngrokNgrokURLError, PyngrokSecurityError, PyngrokError
from pyngrok.installer import install_ngrok, install_default_config

install_aliases()
Expand All @@ -26,7 +26,7 @@

__author__ = "Alex Laird"
__copyright__ = "Copyright 2020, Alex Laird"
__version__ = "4.1.13"
__version__ = "4.1.14"

logger = logging.getLogger(__name__)

Expand All @@ -47,18 +47,26 @@ class NgrokTunnel:
:vartype config: dict
:var metrics: Metrics for `the tunnel <https://ngrok.com/docs#list-tunnels>`_.
:vartype metrics: dict
:var pyngrok_config: The ``pyngrok`` configuration to use with ``ngrok``.
:vartype pyngrok_config: PyngrokConfig
:var api_url: The API URL for the ``ngrok`` web interface.
:vartype api_url: str
"""

def __init__(self, data=None):
def __init__(self, data=None, pyngrok_config=None, api_url=None):
if data is None:
data = {}

self.name = data["name"] if data else None
self.proto = data["proto"] if data else None
self.uri = data["uri"] if data else None
self.public_url = data["public_url"] if data else None
self.config = data["config"] if data else {}
self.metrics = data["metrics"] if data else None
if pyngrok_config is None:
pyngrok_config = conf.DEFAULT_PYNGROK_CONFIG

self.name = data.get("name")
self.proto = data.get("proto")
self.uri = data.get("uri")
self.public_url = data.get("public_url")
self.config = data.get("config", {})
self.metrics = data.get("metrics", {})
self.pyngrok_config = pyngrok_config
self.api_url = api_url

def __repr__(self):
return "<NgrokTunnel: \"{}\" -> \"{}\">".format(self.public_url, self.config["addr"]) if self.config.get(
Expand All @@ -68,6 +76,21 @@ def __str__(self): # pragma: no cover
return "NgrokTunnel: \"{}\" -> \"{}\"".format(self.public_url, self.config["addr"]) if self.config.get(
"addr", None) else "<pending Tunnel>"

def refresh_metrics(self):
"""
Refresh the metrics from the tunnel.
"""
if self.api_url is None:
raise PyngrokError("\"api_url\" was not initialized with this NgrokTunnel, so this method cannot be used.")

data = api_request("{}{}".format(self.api_url, self.uri), method="GET", data=None,
timeout=self.pyngrok_config.request_timeout)

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

self.metrics = data["metrics"]


def ensure_ngrok_installed(ngrok_path):
"""
Expand Down Expand Up @@ -184,7 +207,8 @@ def connect(port="80", proto="http", name=None, options=None, pyngrok_config=Non
logger.debug("Connecting tunnel with options: {}".format(options))

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

if proto == "http" and not options.get("bind_tls", False):
tunnel.public_url = tunnel.public_url.replace("https", "http")
Expand Down Expand Up @@ -250,7 +274,7 @@ def get_tunnels(pyngrok_config=None):
tunnels = []
for tunnel in api_request("{}/api/tunnels".format(api_url), method="GET", data=None,
timeout=pyngrok_config.request_timeout)["tunnels"]:
tunnels.append(NgrokTunnel(tunnel))
tunnels.append(NgrokTunnel(tunnel, pyngrok_config=pyngrok_config, api_url=api_url))

return tunnels

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -4,7 +4,7 @@

__author__ = "Alex Laird"
__copyright__ = "Copyright 2020, Alex Laird"
__version__ = "4.1.13"
__version__ = "4.1.14"

name = "pyngrok" if os.environ.get("BUILD_PACKAGE_AS_NGROK", "False") != "True" else "ngrok"

Expand Down
4 changes: 2 additions & 2 deletions tests/test_installer.py
@@ -1,15 +1,15 @@
import os
import socket

from mock import mock
import mock

from pyngrok import ngrok, installer, conf
from pyngrok.exception import PyngrokNgrokInstallError, PyngrokSecurityError, PyngrokError
from .testcase import NgrokTestCase

__author__ = "Alex Laird"
__copyright__ = "Copyright 2020, Alex Laird"
__version__ = "4.1.0"
__version__ = "4.1.14"


class TestInstaller(NgrokTestCase):
Expand Down
32 changes: 24 additions & 8 deletions tests/test_ngrok.py
Expand Up @@ -16,6 +16,7 @@

install_aliases()

from urllib.parse import urlparse
from urllib.request import urlopen

try:
Expand All @@ -28,7 +29,7 @@

__author__ = "Alex Laird"
__copyright__ = "Copyright 2020, Alex Laird"
__version__ = "4.1.13"
__version__ = "4.1.14"


class TestNgrok(NgrokTestCase):
Expand Down Expand Up @@ -163,13 +164,11 @@ def test_api_request_query_params(self):
# GIVEN
tunnel_name = "tunnel (1)"
current_process = ngrok.get_ngrok_process(pyngrok_config=self.pyngrok_config)
public_url = ngrok.connect(name=tunnel_name).replace("http", "https")
public_url = ngrok.connect(urlparse(current_process.api_url).port, name=tunnel_name).replace("http", "https")
time.sleep(1)

try:
urlopen(public_url)
except:
pass
urlopen(public_url).read()
time.sleep(3)

# WHEN
response1 = ngrok.api_request("{}/api/requests/http".format(current_process.api_url), "GET")
Expand All @@ -179,8 +178,8 @@ def test_api_request_query_params(self):
params={"tunnel_name": "{} (http)".format(tunnel_name)})

# THEN
self.assertEqual(1, len(response1["requests"]))
self.assertEqual(1, len(response2["requests"]))
self.assertGreater(len(response1["requests"]), 0)
self.assertGreater(len(response2["requests"]), 0)
self.assertEqual(0, len(response3["requests"]))

def test_api_request_delete_data_updated(self):
Expand Down Expand Up @@ -389,3 +388,20 @@ def test_get_tunnel_fileserver(self):
# THEN
self.assertEqual(tunnel.name, response["name"])
self.assertIn("file", tunnel.name)

def test_ngrok_tunnel_refresh_metrics(self):
# GIVEN
current_process = ngrok.get_ngrok_process(pyngrok_config=self.pyngrok_config)
public_url = ngrok.connect(urlparse(current_process.api_url).port)
time.sleep(1)
ngrok_tunnel = list(filter(lambda t: t.public_url == public_url, ngrok.get_tunnels()))[0]
self.assertEqual(0, ngrok_tunnel.metrics.get("http").get("count"))

urlopen("{}/status".format(public_url)).read()
time.sleep(3)

# WHEN
ngrok_tunnel.refresh_metrics()

# THEN
self.assertGreater(ngrok_tunnel.metrics.get("http").get("count"), 0)
14 changes: 7 additions & 7 deletions tests/test_process.py
Expand Up @@ -4,7 +4,7 @@
import time

from future.standard_library import install_aliases
from mock import mock
import mock

from pyngrok import process, installer, conf, ngrok
from pyngrok.conf import PyngrokConfig
Expand All @@ -15,10 +15,11 @@
install_aliases()

from urllib.parse import urlparse
from urllib.request import urlopen

__author__ = "Alex Laird"
__copyright__ = "Copyright 2020, Alex Laird"
__version__ = "4.1.9"
__version__ = "4.1.14"


class TestProcess(NgrokTestCase):
Expand Down Expand Up @@ -293,7 +294,9 @@ def test_no_monitor_thread(self):
def test_stop_monitor_thread(self):
# GIVEN
self.given_ngrok_installed(self.pyngrok_config.ngrok_path)
public_url = ngrok.connect(pyngrok_config=self.pyngrok_config)
current_process = ngrok.get_ngrok_process(pyngrok_config=self.pyngrok_config)
public_url = ngrok.connect(urlparse(current_process.api_url).port, options={"bind_tls": True},
pyngrok_config=self.pyngrok_config)
ngrok_process = ngrok.get_ngrok_process()
monitor_thread = ngrok_process._monitor_thread

Expand All @@ -302,10 +305,7 @@ def test_stop_monitor_thread(self):
self.assertTrue(monitor_thread.is_alive())
ngrok_process.stop_monitor_thread()
# Make a request to the tunnel to force a log through, which will allow the thread to trigger its own teardown
try:
ngrok.api_request(public_url)
except:
pass
urlopen("{}/status".format(public_url)).read()
time.sleep(1)

# THEN
Expand Down

0 comments on commit 3bed665

Please sign in to comment.