Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions connect/cli/plugins/project/extension/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


PYPI_EXTENSION_RUNNER_URL = 'https://pypi.org/pypi/connect-extension-runner/json'
PYPI_EXTENSION_RUNNER_RELEASE_URL = 'https://pypi.org/pypi/connect-extension-runner/{version}/json'

PRE_COMMIT_HOOK = """#! /bin/sh

Expand Down
7 changes: 6 additions & 1 deletion connect/cli/plugins/project/extension/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from connect.cli.core.terminal import console
from connect.cli.plugins.project.extension.utils import (
get_event_definitions,
get_pypi_runner_requirements,
get_pypi_runner_version,
get_pypi_runner_version_by_connect_version,
initialize_git_repository,
Expand Down Expand Up @@ -79,11 +80,15 @@ def bootstrap_extension_project( # noqa: CCR001
if not answers:
raise ClickException('Aborted by user input')

runner_version = get_pypi_runner_version()
connect_eaas_core_version, python_version = get_pypi_runner_requirements(runner_version)
ctx = {
'statuses_by_event': statuses_by_event[answers['extension_type']],
'interactive': [],
'current_major_version': get_version().split('.')[0],
'runner_version': get_pypi_runner_version(),
'runner_version': runner_version,
'connect_eaas_core_version': connect_eaas_core_version,
'python_version': python_version,
}

for var, answer in answers.items():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,26 @@ readme = "./README.md"
{%- endif %}

[tool.poetry.dependencies]
python = ">=3.8,<4"
connect-eaas-core = ">=30"
python = "{{ python_version }}"
connect-eaas-core = "{{ connect_eaas_core_version }}"

[tool.poetry.dev-dependencies]
pytest = ">=6.1.2,<8"
pytest-cov = ">=2.10.1,<5"
pytest = ">=8,<9"
pytest-cov = ">=2.10.1,<7"
pytest-mock = "^3.3.1"
mock = { version = "^4.0.3", markers = "python_version < '3.8'" }
coverage = {extras = ["toml"], version = ">=5.3,<7"}
flake8 = ">=3.8,<6"
flake8-bugbear = ">=20,<23"
coverage = {extras = ["toml"], version = ">=5.3,<8"}
flake8 = ">=6"
flake8-bugbear = ">=24"
flake8-cognitive-complexity = "^0.1"
flake8-commas = "~2.0"
flake8-commas = "~4"
flake8-future-import = "~0.4"
flake8-import-order = "~0.18"
flake8-broken-line = ">=0.3,<0.7"
flake8-comprehensions = "^3.3.1"
flake8-broken-line = ">=1.0"
flake8-comprehensions = "^3.10"
flake8-debugger = "^4.0.0"
flake8-eradicate = "^1.0.0"
flake8-eradicate = "^1.3.0"
flake8-string-format = "^0.3.0"
pytest-asyncio = "^0.15.1"
pytest-asyncio = ">=0.23"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
38 changes: 31 additions & 7 deletions connect/cli/plugins/project/extension/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import requests
from click import ClickException
from connect.client import ClientError
from packaging.requirements import Requirement

from connect.cli import get_version
from connect.cli.core.utils import (
get_connect_version,
get_last_version_by_major,
sort_and_filter_tags,
)
from connect.cli.plugins.project.extension.constants import (
PRE_COMMIT_HOOK,
PYPI_EXTENSION_RUNNER_RELEASE_URL,
PYPI_EXTENSION_RUNNER_URL,
)

Expand All @@ -31,11 +31,35 @@ def get_pypi_runner_version():
raise ClickException(
f'We can not retrieve the current connect-extension-runner version from {PYPI_EXTENSION_RUNNER_URL}.',
)
content = res.json()
tags = sort_and_filter_tags(content['releases'], get_version().split('.', 1)[0])
if tags:
return tags.popitem()[0]
return content['info']['version']
return res.json()['info']['version']


def get_pypi_runner_requirements(runner_version):
"""Return the (connect-eaas-core, python) version specifiers required by the given runner.

The bootstrapped extension must pin the same connect-eaas-core and python range
the runner image ships, otherwise `poetry update` resolves an incompatible
version and the extension fails to run.
"""
url = PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version=runner_version)
res = requests.get(url)
if res.status_code != 200:
raise ClickException(
f'We can not retrieve the connect-extension-runner {runner_version} metadata from {url}.',
)
info = res.json()['info']
requires_python = info.get('requires_python')
if not requires_python:
raise ClickException(
f'connect-extension-runner {runner_version} does not declare a python requirement.',
)
for dist in info.get('requires_dist') or []:
requirement = Requirement(dist)
if requirement.name == 'connect-eaas-core':
return str(requirement.specifier), requires_python
raise ClickException(
f'connect-extension-runner {runner_version} does not declare a connect-eaas-core dependency.',
)


def get_pypi_runner_version_by_connect_version():
Expand Down
39 changes: 38 additions & 1 deletion tests/plugins/project/test_extension_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def test_bootstrap_extension_project_background(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_vendor.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -127,6 +131,9 @@ def test_bootstrap_extension_project_background(

pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml'))

assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '<38,>=37.4'
assert pyproject_toml['tool']['poetry']['dependencies']['python'] == '<3.13,>=3.9'

ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext']
assert ext_entrypoint == {
'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication",
Expand Down Expand Up @@ -224,6 +231,10 @@ def test_bootstrap_extension_project_interactive(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_vendor.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -296,6 +307,9 @@ def test_bootstrap_extension_project_interactive(

pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml'))

assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '<38,>=37.4'
assert pyproject_toml['tool']['poetry']['dependencies']['python'] == '<3.13,>=3.9'

ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext']
assert ext_entrypoint == {
'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication",
Expand Down Expand Up @@ -377,6 +391,10 @@ def test_bootstrap_extension_project_multiaccount(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_vendor.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -527,6 +545,10 @@ def test_bootstrap_extension_project_webapp(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_provider.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -688,6 +710,10 @@ def test_bootstrap_extension_project_tfnapp(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_provider.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -893,7 +919,6 @@ def test_bootstrap_extension_project_if_destination_exists(mocker):


def test_bump_runner_version(mocker, mocked_responses, capsys):
mocker.patch('connect.cli.plugins.project.extension.utils.get_version', return_value='0.5')
mocked_responses.add(
'GET',
DEFAULT_ENDPOINT,
Expand Down Expand Up @@ -1045,6 +1070,10 @@ def test_validate_extension_project(mocker, faker, mocked_responses, config_vend
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_vendor.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -1114,6 +1143,10 @@ def test_validate_extension_project_error_exit(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
side_effect=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_vendor.load(config_dir='/tmp')

mocked_responses.add(
Expand Down Expand Up @@ -1184,6 +1217,10 @@ def test_bootstrap_extension_project_corrupted_load_answers(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version',
return_value=runner_version,
)
mocker.patch(
'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements',
return_value=('<38,>=37.4', '<3.13,>=3.9'),
)
config_provider.load(config_dir='/tmp')

mocked_responses.add(
Expand Down
108 changes: 82 additions & 26 deletions tests/plugins/project/test_extension_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from click import ClickException
from connect.client import ClientError

from connect.cli.plugins.project.extension.constants import PYPI_EXTENSION_RUNNER_URL
from connect.cli.plugins.project.extension.constants import (
PYPI_EXTENSION_RUNNER_RELEASE_URL,
PYPI_EXTENSION_RUNNER_URL,
)
from connect.cli.plugins.project.extension.utils import (
check_eventsapp_feature_not_selected,
check_extension_events_applicable,
Expand All @@ -14,37 +17,32 @@
get_default_application_types,
get_event_definitions,
get_interactive_events,
get_pypi_runner_requirements,
get_pypi_runner_version,
)


def test_get_pypi_runner_version(mocker, mocked_responses):
mocker.patch(
'connect.cli.plugins.project.extension.utils.get_version',
return_value='25.0',
)
def test_get_pypi_runner_version(mocked_responses):
"""The bootstrap must pin the latest published runner, not one tied to the CLI major.

The CLI major (26) diverged from the runner line (43), so filtering runner tags by
CLI major returned a stale, incompatible image. Confidence: a fresh extension always
gets the current runner.
"""
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_URL,
json={
'releases': {
'26.0': {},
'25.1': {},
'25.0': {},
'24.5': {},
},
'info': {'version': '43.0'},
'releases': {'43.0': {}, '42.0': {}, '26.44': {}},
},
status=200,
)

assert get_pypi_runner_version() == '25.1'
assert get_pypi_runner_version() == '43.0'


def test_get_pypi_runner_version_http_error(mocker, mocked_responses):
mocker.patch(
'connect.cli.plugins.project.extension.utils.get_version',
return_value='25.0',
)
def test_get_pypi_runner_version_http_error(mocked_responses):
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_URL,
Expand All @@ -56,22 +54,80 @@ def test_get_pypi_runner_version_http_error(mocker, mocked_responses):
assert 'We can not retrieve the current connect-extension-runner version' in str(ve.value)


def test_get_pypi_runner_version_no_releases(mocker, mocked_responses):
mocker.patch(
'connect.cli.plugins.project.extension.utils.get_version',
return_value='25.0',
def test_get_pypi_runner_requirements(mocked_responses):
"""The extension must pin the connect-eaas-core and python range the runner ships.

If the generated pyproject allows versions the runner does not support,
`poetry update` resolves an incompatible version and the extension fails to run.
Confidence: the runner↔eaas-core↔python triple is always coherent, so the
extension boots.
"""
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'),
status=200,
json={
'info': {
'requires_python': '<3.13,>=3.9',
'requires_dist': [
'logzio-python-handler>=3.1',
'connect-eaas-core<38,>=37.4',
],
},
},
)

assert get_pypi_runner_requirements('43.0') == ('<38,>=37.4', '<3.13,>=3.9')


def test_get_pypi_runner_requirements_http_error(mocked_responses):
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_URL,
url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'),
status=500,
)
with pytest.raises(ClickException) as ve:
get_pypi_runner_requirements('43.0')

assert 'We can not retrieve the connect-extension-runner 43.0 metadata' in str(ve.value)


def test_get_pypi_runner_requirements_missing_python(mocked_responses):
"""A runner that omits requires_python must fail loudly, not render an empty pin."""
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'),
status=200,
json={'info': {'requires_dist': ['connect-eaas-core<38,>=37.4']}},
)

with pytest.raises(ClickException) as ve:
get_pypi_runner_requirements('43.0')

assert 'does not declare a python requirement' in str(ve.value)


def test_get_pypi_runner_requirements_missing_dependency(mocked_responses):
"""A runner that omits connect-eaas-core must fail loudly, not silently mis-pin.

Confidence: we never generate a pyproject with an empty/invalid eaas-core constraint.
"""
mocked_responses.add(
method='GET',
url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'),
status=200,
json={
'info': {'version': '26.0'},
'releases': {},
'info': {
'requires_python': '<3.13,>=3.9',
'requires_dist': ['logzio-python-handler>=3.1'],
},
},
)

assert get_pypi_runner_version() == '26.0'
with pytest.raises(ClickException) as ve:
get_pypi_runner_requirements('43.0')

assert 'does not declare a connect-eaas-core dependency' in str(ve.value)


def test_get_event_definitions_client_error(mocker):
Expand Down
Loading