Skip to content

Commit

Permalink
feat: prevent Writer Framework version mismatch local/cloud
Browse files Browse the repository at this point in the history
* refact: improve wf_project module readability
  • Loading branch information
FabienArcellier committed Nov 11, 2024
1 parent b87b9aa commit 9fca105
Showing 1 changed file with 87 additions and 48 deletions.
135 changes: 87 additions & 48 deletions src/writer/wf_project.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
"""
This module manipulates the folder of a wf project stored into `wf`.
This module manages the characteristics of the project :
>>> wf_project.write_files('app/hello', metadata={"writer_version": "0.1" }, components=...)
* the .wf folder that contains the metadata, ui definition and workflow definition
* the migration of previous project architecture (using ui.json) into new one (using .wf folder)
* the writer version used during development and locked into dependencies
>>> wf_project.write_files('app/hello', metadata={"writer_version": "0.1" }, components=...)
>>> metadata, components = wf_project.read_files('app/hello')
>>> wf_project.use_poetry('app/hello')
>>> wf_project.use_requirement('app/hello')
>>> v = wf_project.poetry_locked_version('app/hello')
"""
import io
import json
Expand All @@ -21,6 +28,7 @@
ROOTS = ['root', 'workflows_root']
COMPONENT_ROOTS = ['page', 'workflows_workflow']


def write_files(app_path: str, metadata: MetadataDefinition, components: Dict[str, ComponentDefinition]) -> None:
"""
Writes the meta data of the WF project to the `.wf` directory (metadata, components, ...).
Expand Down Expand Up @@ -119,38 +127,53 @@ def migrate_obsolete_ui_json(app_path: str, metadata: MetadataDefinition) -> Non
logger.warning('project format has changed and has been migrated with success. ui.json file has been removed.')


def create_default_workflows_root(abs_path: str) -> None:
with io.open(os.path.join(abs_path, '.wf', 'components-workflows_root.jsonl'), 'w') as f:
def create_default_workflows_root(app_path: str) -> None:
"""
Creates .wf/components-workflows_root.jsonl. This file should be present for the project to be valid.
>>> wf_project.create_default_workflows_root('app/hello')
"""
with io.open(os.path.join(app_path, '.wf', 'components-workflows_root.jsonl'), 'w') as f:
f.write('{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}')
logger = logging.getLogger('writer')
logger.warning('project format has changed and has been migrated with success. components-workflows_root.jsonl has been added.')


def _expected_component_fileinfos(components: dict[str, ComponentDefinition]) -> List[Tuple[str, str]]:
def compare_semantic_version(version1: SemanticVersion, version2: SemanticVersion) -> int:
"""
Returns the list of component file information to write (id, filename).
Compares two versions that support semantic version (major, minor, fix, rc).
>>> component_files = expected_component_files(components)
>>> for component_id, filename in component_files:
>>> print(f"Components that depends on {component_id} will be written in {filename}")
* 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
"""
expected_component_files = []
for component_file_root in COMPONENT_ROOTS:
position = 0
for c in components.values():
if c["type"] == component_file_root:
filename = f"components-{ component_file_root }-{position}-{c['id']}.jsonl"
expected_component_files.append((c["id"], filename))
position += 1
if version1 == version2:
return 0

return expected_component_files
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 use_poetry(app_path: str) -> bool:
"""
Checks if the application uses poetry
Checks if the application uses poetry. Checks if the pyproject.tml file and if the build system is that of poetry.
:param app_path: absolute path of the application
:param app_path: path of the application
:return: True if the application uses poetry
"""
pyproject_path = os.path.join(app_path, 'pyproject.toml')
Expand All @@ -166,7 +189,21 @@ def use_poetry(app_path: str) -> bool:

return False


def writer_version(app_path: str) -> typing.Optional[SemanticVersion]:
"""
Retrieves the writer version used during development
>>> v = writer_version('app/hello')
The version is returned as a tuple (major, minor, fix, rc) or None if not found
* 1.0.0 -> (1, 0, 0, None)
* 1.0.0rc1 -> (1, 0, 0, 1)
:param app_path: path of the application
:return: the version of writer used during development or None
"""
meta, _ = read_files(app_path)
writer_version = meta.get('writer_version', None)
if writer_version:
Expand All @@ -180,8 +217,13 @@ def poetry_locked_version(app_path: str) -> typing.Optional[SemanticVersion]:
If the application does not have a poetry.lock file, the function returns None
The version is returned as a tuple (major, minor, fix, rc) or None if not found
* 1.0.0 -> (1, 0, 0, None)
* 1.0.0rc1 -> (1, 0, 0, 1)
:param app_path: absolute path of the application
:return: the version fixed in the poetry.lock file for Writer
:return: the version of writer freezed in the poetry.lock file
"""
poetry_lock_path = os.path.join(app_path, 'poetry.lock')
if not os.path.isfile(poetry_lock_path):
Expand All @@ -198,34 +240,28 @@ def poetry_locked_version(app_path: str) -> typing.Optional[SemanticVersion]:
return None


def compare_semantic_version(version1: SemanticVersion, version2: SemanticVersion) -> int:
"""
Compares two versions that support semantic version (major, minor, fix, rc).
def use_requirement(app_path: str):
return None

* 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
def _expected_component_fileinfos(components: dict[str, ComponentDefinition]) -> List[Tuple[str, str]]:
"""
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
Returns the list of component file information to write (id, filename).
if version2[3] is None and version1[3] is not None:
return -1
>>> component_files = expected_component_files(components)
>>> for component_id, filename in component_files:
>>> print(f"Components that depends on {component_id} will be written in {filename}")
"""
expected_component_files = []
for component_file_root in COMPONENT_ROOTS:
position = 0
for c in components.values():
if c["type"] == component_file_root:
filename = f"components-{ component_file_root }-{position}-{c['id']}.jsonl"
expected_component_files.append((c["id"], filename))
position += 1

return 0
return expected_component_files


def _list_component_files(wf_directory: str) -> List[str]:
Expand Down Expand Up @@ -305,7 +341,14 @@ def _hierarchical_position(components, c):

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


def _parse_semantic_version(version: str) -> SemanticVersion:
"""
Parses a version string into a semantic version tuple (major, minor, fix, rc)
>>> _parse_semantic_version('1.0.0') # (1, 0, 0, None)
>>> _parse_semantic_version('1.0.0rc1') # (1, 0, 0, 1)
"""
major, minor, fix_part = version.split('.')
if 'rc' in fix_part:
fix, rc = fix_part.split('rc')
Expand All @@ -319,7 +362,3 @@ def _parse_semantic_version(version: str) -> SemanticVersion:
semantic_version = int(major), int(minor), int(fix), None

return semantic_version


def use_requirement(abs_path: str):
return None

0 comments on commit 9fca105

Please sign in to comment.