diff --git a/src/ploomber/cli/cloud.py b/src/ploomber/cli/cloud.py index 182a68c68..e3beffaf0 100644 --- a/src/ploomber/cli/cloud.py +++ b/src/ploomber/cli/cloud.py @@ -15,6 +15,7 @@ import http.client as httplib import click from functools import wraps +import re import humanize @@ -22,8 +23,9 @@ from ploomber.telemetry import telemetry from ploomber.telemetry.telemetry import parse_dag, UserSettings -CLOUD_APP_URL = 'ggeheljnx2.execute-api.us-east-1.amazonaws.com' -PIPELINES_RESOURCE = '/prod/pipelines' +CLOUD_APP_URL = 'api.ploomber.io' +PIPELINES_RESOURCE = '/pipelines' +EMAIL_RESOURCE = '/emailSignup' headers = {'Content-type': 'application/json'} @@ -236,3 +238,41 @@ def wrapper(*args, **kwargs): return wrapper return _cloud_call + + +def _get_input(text): + return input(text) + + +def _email_input(): + # Validate that's the first email registration + settings = UserSettings() + if not settings.user_email: + email = _get_input( + "Our users enjoy updates, support and unique content " + "through email, please add your email, if you'd like " + "to register (type email): ") + _email_validation(email) + + +def _email_validation(email): + pattern = r"[^@]+@[^@]+\.[^@]+" + if re.match(pattern, email): + # Save in conf file + settings = UserSettings() + settings.user_email = email + + # Call API + _email_registry(email) + + +def _email_registry(email): + conn = httplib.HTTPSConnection(CLOUD_APP_URL, timeout=3) + try: + user_headers = {'email': email, 'source': 'OS'} + conn.request("POST", EMAIL_RESOURCE, headers=user_headers) + print("Thanks for signing up!") + except httplib.HTTPException: + pass + finally: + conn.close() diff --git a/src/ploomber/cli/examples.py b/src/ploomber/cli/examples.py index 0bb4d24e2..da4427c06 100644 --- a/src/ploomber/cli/examples.py +++ b/src/ploomber/cli/examples.py @@ -16,6 +16,7 @@ from ploomber.table import Table from ploomber.telemetry import telemetry from ploomber.exceptions import BaseException +from ploomber.cli.cloud import _email_input from pygments.formatters.terminal import TerminalFormatter from pygments.lexers.markup import MarkdownLexer @@ -287,3 +288,4 @@ def main(name, force=False, branch=None, output=None): f'\n$ ploomber install') tw.write(f'\n\nOpen {str(path_to_readme)} for details.\n', blue=True) + _email_input() diff --git a/src/ploomber/telemetry/telemetry.py b/src/ploomber/telemetry/telemetry.py index 21faf61a1..6d708e0ae 100644 --- a/src/ploomber/telemetry/telemetry.py +++ b/src/ploomber/telemetry/telemetry.py @@ -59,6 +59,7 @@ class UserSettings(Config): """ version_check_enabled: bool = True cloud_key: str = None + user_email: str = None stats_enabled: bool = True @classmethod @@ -302,6 +303,16 @@ def is_cloud_user(): return settings.cloud_key +def email_registered(): + """ + The function checks if the email is set for the user. + Checks if the user_email is set in the User conf file (config.yaml). + returns True/False accordingly. + """ + settings = UserSettings() + return settings.user_email + + def check_version(): """ The function checks if the user runs the latest version @@ -395,6 +406,7 @@ def log_api(action, client_time=None, total_runtime=None, metadata=None): py_version = python_version() docker_container = is_docker() cloud = is_cloud_user() + email = email_registered() colab = is_colab() if colab: metadata['colab'] = colab @@ -438,6 +450,7 @@ def log_api(action, client_time=None, total_runtime=None, metadata=None): 'ploomber_version': product_version, 'docker_container': docker_container, 'cloud': cloud, + 'email': email, 'os': os, 'environment': environment, 'metadata': metadata, diff --git a/tests/cli/test_cloud.py b/tests/cli/test_cloud.py index eb03423a3..323d1bc19 100644 --- a/tests/cli/test_cloud.py +++ b/tests/cli/test_cloud.py @@ -6,7 +6,7 @@ import yaml from click.testing import CliRunner -from ploomber.cli import cloud +from ploomber.cli import cloud, examples from ploomber_cli.cli import get_key, set_key, write_pipeline, get_pipelines,\ delete_pipeline from ploomber.telemetry import telemetry @@ -387,3 +387,71 @@ def test_get_pipeline_with_dag(monkeypatch, mock_api_key): # id = p['pipeline_id'] # res = cloud.delete_pipeline(id) # assert id in str(res) + + +# Test empty string/emails without a @ +@pytest.mark.parametrize('user_email', ['', 'test', '@', 'a@c']) +def test_malformed_email_signup(monkeypatch, user_email): + mock = Mock() + monkeypatch.setattr(cloud, '_email_registry', mock) + + cloud._email_validation(user_email) + mock.assert_not_called() + + +# Testing valid api calls when the email is correct +def test_correct_email_signup(tmp_directory, monkeypatch): + monkeypatch.setattr(telemetry, 'DEFAULT_HOME_DIR', '.') + registry_mock = Mock() + monkeypatch.setattr(cloud, '_email_registry', registry_mock) + + sample_email = 'test@example.com' + cloud._email_validation(sample_email) + registry_mock.assert_called_once() + + +# Test valid emails are stored in the user conf +def test_email_conf_file(tmp_directory, monkeypatch): + registry_mock = Mock() + monkeypatch.setattr(cloud, '_email_registry', registry_mock) + monkeypatch.setattr(telemetry, 'DEFAULT_HOME_DIR', '.') + + stats = Path('stats') + stats.mkdir() + conf_path = stats / telemetry.DEFAULT_USER_CONF + conf_path.write_text("sample_conf_key: True\n") + + sample_email = 'test@example.com' + cloud._email_validation(sample_email) + + conf = conf_path.read_text() + assert sample_email in conf + + +def test_email_write_only_once(tmp_directory, monkeypatch): + monkeypatch.setattr(telemetry, 'DEFAULT_HOME_DIR', '.') + input_mock = Mock(return_value='some1@email.com') + monkeypatch.setattr(cloud, '_get_input', input_mock) + monkeypatch.setattr(telemetry.UserSettings, 'user_email', 'some@email.com') + + cloud._email_input() + assert not input_mock.called + + +def test_email_call_on_examples(tmp_directory, monkeypatch): + email_mock = Mock() + monkeypatch.setattr(examples, '_email_input', email_mock) + examples.main(name=None, force=True) + email_mock.assert_called_once() + + +def test_email_called_once(tmp_directory, monkeypatch): + monkeypatch.setattr(telemetry, 'DEFAULT_HOME_DIR', '.') + email_mock = Mock(return_value='email@ploomber.io') + api_mock = Mock() + monkeypatch.setattr(cloud, '_get_input', email_mock) + monkeypatch.setattr(cloud, '_email_registry', api_mock) + + examples.main(name=None, force=True) + examples.main(name=None, force=True) + email_mock.assert_called_once() diff --git a/tests/cli/test_examples.py b/tests/cli/test_examples.py index 4ecb53fd7..141dbc58c 100644 --- a/tests/cli/test_examples.py +++ b/tests/cli/test_examples.py @@ -17,8 +17,8 @@ def _mock_metadata(**kwargs): return {**default, **kwargs} -@pytest.fixture(scope='session') -def clone_examples(): +@pytest.fixture(scope='function') +def clone_examples(_mock_email): examples.main(name=None, force=True) @@ -101,7 +101,7 @@ def test_click_exception_isnt_shadowed_by_runtime_error(monkeypatch): assert 'Error: some click exception\n' in result.output -def test_clones_in_home_directory(monkeypatch, tmp_directory): +def test_clones_in_home_directory(_mock_email, monkeypatch, tmp_directory): # patch home directory monkeypatch.setattr(examples, '_home', str(tmp_directory)) @@ -123,7 +123,7 @@ def test_clones_in_home_directory(monkeypatch, tmp_directory): check=True) -def test_change_default_branch(monkeypatch, tmp_directory): +def test_change_default_branch(_mock_email, monkeypatch, tmp_directory): # mock metadata to make it look older metadata = _mock_metadata(timestamp=(datetime.now() - timedelta(days=1)).timestamp()) @@ -149,7 +149,7 @@ def test_change_default_branch(monkeypatch, tmp_directory): def test_does_not_download_again_if_no_explicit_branch_requested( - monkeypatch, tmp_directory): + _mock_email, monkeypatch, tmp_directory): dir_ = Path(tmp_directory, 'examples') monkeypatch.setattr(examples, '_home', dir_) diff --git a/tests/conftest.py b/tests/conftest.py index 24b5e5ec3..582a9fba9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ from ploomber import Env import pandas as pd from glob import iglob -from ploomber.cli import install +from ploomber.cli import install, examples import posthog from unittest.mock import Mock, MagicMock @@ -469,3 +469,9 @@ def tmp_imports(add_current_to_sys_path, no_sys_modules_cache): test execution upon exit """ yield + + +@pytest.fixture +def _mock_email(monkeypatch): + examples_email_mock = Mock() + monkeypatch.setattr(examples, '_email_input', examples_email_mock) diff --git a/tests/telemetry/test_telemetry.py b/tests/telemetry/test_telemetry.py index 73f652f4d..9963c1556 100644 --- a/tests/telemetry/test_telemetry.py +++ b/tests/telemetry/test_telemetry.py @@ -58,6 +58,7 @@ def test_user_settings_create_file(tmp_directory, monkeypatch): assert content == { 'cloud_key': None, + 'user_email': None, 'stats_enabled': True, 'version_check_enabled': True, }