Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RC for 4.1.14 #70

Merged
merged 23 commits into from Oct 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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