Skip to content

Commit

Permalink
feat: prevent Writer Framework version mismatch local/cloud
Browse files Browse the repository at this point in the history
* feat: implement poetry verification
  • Loading branch information
FabienArcellier committed Nov 8, 2024
1 parent 00dcd1d commit b87b9aa
Show file tree
Hide file tree
Showing 8 changed files with 2,570 additions and 3 deletions.
1,209 changes: 1,209 additions & 0 deletions apps/default/poetry.lock

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ watchdog = ">= 3.0.0, < 4"
websockets = ">= 12, < 13"
writer-sdk = ">= 1.2.0, < 2"
python-multipart = ">=0.0.7, < 1"
toml = "^0.10.2"


[tool.poetry.group.build]
Expand All @@ -71,6 +72,7 @@ types-requests = "^2.31.0.20240406"

[tool.poetry.group.dev.dependencies]
types-python-dateutil = "^2.9.0.20240316"
types-toml = "^0.10.8.20240310"

[tool.poetry.scripts]
writer = 'writer.command_line:main'
Expand Down
12 changes: 12 additions & 0 deletions src/writer/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import requests
from gitignore_parser import parse_gitignore

from writer import wf_project


@click.group()
def cloud():
Expand Down Expand Up @@ -47,6 +49,15 @@ def deploy(path, api_key, env, verbose, force):
if not os.path.isdir(abs_path):
raise click.ClickException("A path to a folder containing a Writer Framework app is required. For example: writer cloud deploy my_app")

if wf_project.use_poetry(abs_path):
poetry_locked_version = wf_project.poetry_locked_version(abs_path)
if poetry_locked_version is None:
raise click.ClickException("The application does not have a poetry.lock file. Please run `poetry install` to generate it.")

writer_version = wf_project.writer_version(abs_path)
if poetry_locked_version != writer_version:
raise click.ClickException(f"locked version in poetry.lock does not match the project version use in .wf, pin a new version `poetry add writer@^{writer_version}")

env = _validate_env_vars(env)
tar = pack_project(abs_path)
try:
Expand Down Expand Up @@ -294,3 +305,4 @@ def unauthorized_error():
print("Unauthorized. Please check your API key.")
sys.exit(1)


6 changes: 5 additions & 1 deletion src/writer/ss_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,9 @@ class ComponentDefinition(TypedDict):
x: Optional[int]
y: Optional[int]


class WorkflowExecutionLog(BaseModel):
summary: List[Dict]
summary: List[Dict]


SemanticVersion = Tuple[int, int, int, Optional[int]] # major, minor, fix, rc
104 changes: 103 additions & 1 deletion src/writer/wf_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
from collections import OrderedDict
from typing import Any, Dict, List, Tuple

import toml

from writer import core_ui
from writer.ss_types import ComponentDefinition, MetadataDefinition
from writer.ss_types import ComponentDefinition, MetadataDefinition, SemanticVersion

ROOTS = ['root', 'workflows_root']
COMPONENT_ROOTS = ['page', 'workflows_workflow']
Expand Down Expand Up @@ -144,6 +146,88 @@ def _expected_component_fileinfos(components: dict[str, ComponentDefinition]) ->
return expected_component_files


def use_poetry(app_path: str) -> bool:
"""
Checks if the application uses poetry
:param app_path: absolute path of the application
:return: True if the application uses poetry
"""
pyproject_path = os.path.join(app_path, 'pyproject.toml')
has_pyproject = os.path.isfile(pyproject_path)
if not has_pyproject:
return False

with io.open(pyproject_path, 'r') as f:
pyproject = toml.load(f)
build_system = pyproject.get('build-system', {}).get('requires', None)
if build_system == 'poetry.core.masonry.api':
return True

return False

def writer_version(app_path: str) -> typing.Optional[SemanticVersion]:
meta, _ = read_files(app_path)
writer_version = meta.get('writer_version', None)
if writer_version:
return _parse_semantic_version(writer_version)

return None

def poetry_locked_version(app_path: str) -> typing.Optional[SemanticVersion]:
"""
Retrieves the version fixed in the poetry.lock file for Writer
If the application does not have a poetry.lock file, the function returns None
:param app_path: absolute path of the application
:return: the version fixed in the poetry.lock file for Writer
"""
poetry_lock_path = os.path.join(app_path, 'poetry.lock')
if not os.path.isfile(poetry_lock_path):
return None

if os.path.isfile(poetry_lock_path):
with open(poetry_lock_path) as f:
poetry_lock = toml.load(f)
for package in poetry_lock['package']:
if package['name'] == 'writer':
locked_version = package['version']
return locked_version

return None


def compare_semantic_version(version1: SemanticVersion, version2: SemanticVersion) -> int:
"""
Compares two versions that support semantic version (major, minor, fix, rc).
* 1 when version1 > version2
* 0 when version1 == version2
* -1 if version1 < version2
>>> compare_semantic_version((1, 0, 0, None), (1, 0, 0, 1)) # 1
>>> compare_semantic_version((1, 0, 0, 1), (1, 0, 0, 4)) # -1
>>> compare_semantic_version((1, 0, 0, 1), (1, 0, 0, 1)) # 0
"""
if version1 == version2:
return 0

if version1[:3] > version2[:3]:
return 1

if version1[:3] < version2[:3]:
return -1

if version1[3] is None and version2[3] is not None:
return 1

if version2[3] is None and version1[3] is not None:
return -1

return 0


def _list_component_files(wf_directory: str) -> List[str]:
"""
List the component files of the .wf folder.
Expand Down Expand Up @@ -221,3 +305,21 @@ def _hierarchical_position(components, c):

return sorted(components.values(), key=lambda c: _hierarchical_position(components, c))

def _parse_semantic_version(version: str) -> SemanticVersion:
major, minor, fix_part = version.split('.')
if 'rc' in fix_part:
fix, rc = fix_part.split('rc')
else:
fix, rc = fix_part, None

semantic_version: SemanticVersion
if rc is not None:
semantic_version = int(major), int(minor), int(fix), int(rc)
else:
semantic_version = int(major), int(minor), int(fix), None

return semantic_version


def use_requirement(abs_path: str):
return None
Loading

0 comments on commit b87b9aa

Please sign in to comment.