diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1809965..425686c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,6 +14,11 @@ "vscode": { // Set *default* container specific settings.json values on container create. "settings": { + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, "python.pythonPath": "/usr/bin/python3", "python.defaultInterpreterPath": "/usr/bin/python3", "[python]": { @@ -46,5 +51,6 @@ } }, // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh", "remoteUser": "vscode" } diff --git a/.devcontainer/scripts/onCreateCommand.sh b/.devcontainer/scripts/onCreateCommand.sh new file mode 100644 index 0000000..09cd97d --- /dev/null +++ b/.devcontainer/scripts/onCreateCommand.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright (c) 2022-2024 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +echo "#######################################################" +echo "### Install python requirements ###" +echo "#######################################################" +# Update pip before installing requirements +pip3 install --upgrade pip +pip3 install -r requirements.txt +pip3 install -r tests/requirements.txt + +# Add repo to git safe.directory +REPO=$(pwd) +git config --global --add safe.directory $REPO + +# Add git name and email from env variables +if [[ -n "${GIT_CONFIG_NAME}" && -n "${GIT_CONFIG_EMAIL}" ]]; then + git config --global user.name $GIT_CONFIG_NAME + git config --global user.email $GIT_CONFIG_EMAIL +fi diff --git a/docs/index.md b/docs/index.md index 4153199..3249036 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,6 +36,8 @@ - [`velocitas_lib.get_script_path`](./velocitas_lib.md#function-get_script_path): Return the absolute path to the directory the invoked Python script - [`velocitas_lib.get_valid_arch`](./velocitas_lib.md#function-get_valid_arch): Return a known architecture for the given `arch`. - [`velocitas_lib.get_workspace_dir`](./velocitas_lib.md#function-get_workspace_dir): Return the workspace directory. +- [`velocitas_lib.is_uri`](./velocitas_lib.md#function-is_uri): Check if the provided path is a URI. +- [`velocitas_lib.obtain_local_file_path`](./velocitas_lib.md#function-obtain_local_file_path): Return the absolute path to the file, specified by a absolute/relative local path or with an URI. - [`velocitas_lib.replace_in_file`](./velocitas_lib.md#function-replace_in_file): Replace all occurrences of text in a file with a replacement. - [`velocitas_lib.require_env`](./velocitas_lib.md#function-require_env): Require and return an environment variable. - [`velocitas_lib.to_camel_case`](./velocitas_lib.md#function-to_camel_case): Return a camel case version of a snake case string. diff --git a/docs/velocitas_lib.md b/docs/velocitas_lib.md index 1f3283f..63c24ac 100644 --- a/docs/velocitas_lib.md +++ b/docs/velocitas_lib.md @@ -335,6 +335,60 @@ download_file(uri: str, local_file_path: str) → None +--- + + + +## function `is_uri` + +```python +is_uri(path: str) → bool +``` + +Check if the provided path is a URI. + + + +**Args:** + + - `path` (str): The path to check. + + + +**Returns:** + + - `bool`: True if the path is a URI. False otherwise. + + +--- + + + +## function `obtain_local_file_path` + +```python +obtain_local_file_path( + path_or_uri: str, + download_path: Optional[str] = None +) → str +``` + +Return the absolute path to the file, specified by a absolute/relative local path or with an URI. + + + +**Args:** + + - `path_or_uri` (str): Absolute/relative local path or URI. + - `download_path` (str): The path to download the file. + + + +**Returns:** + + - `str`: The absolute path to the file. + + --- diff --git a/tests/test_velocitas_lib.py b/tests/test_velocitas_lib.py index 5adf3b4..b947ffa 100644 --- a/tests/test_velocitas_lib.py +++ b/tests/test_velocitas_lib.py @@ -25,10 +25,12 @@ from velocitas_lib import ( get_app_manifest, get_cache_data, + obtain_local_file_path, get_package_path, get_script_path, get_workspace_dir, require_env, + get_project_cache_dir, ) from velocitas_lib.services import get_services @@ -45,6 +47,18 @@ def set_velocitas_workspace_dir() -> str: return os.environ["VELOCITAS_WORKSPACE_DIR"] +@pytest.fixture() +def set_velocitas_package_dir() -> str: + os.environ["VELOCITAS_PACKAGE_DIR"] = "./tests/package" + return os.environ["VELOCITAS_PACKAGE_DIR"] + + +@pytest.fixture() +def set_velocitas_cache_dir() -> str: + os.environ["VELOCITAS_CACHE_DIR"] = "/test/cache" + return os.environ["VELOCITAS_CACHE_DIR"] + + @pytest.fixture() def set_app_manifest() -> str: app_manifest = {"vehicleModel": {"src": "test"}} @@ -101,7 +115,7 @@ def test_get_script_path__returns_script_path(): assert get_script_path() == os.path.dirname(os.path.realpath(sys.argv[0])) -def test_get_package_path__returns_package_path(): +def test_get_package_path__returns_package_path(set_velocitas_package_dir): assert get_package_path() == "./tests/package" @@ -111,6 +125,7 @@ def test_get_cache_data__returns_cache_data(set_velocitas_cache_data): # type: def test_get_services__no_overwrite_provided__returns_default_services( + set_velocitas_package_dir, mock_filesystem: FakeFilesystem, ): os.environ["runtimeFilePath"] = "runtime.json" @@ -127,6 +142,7 @@ def test_get_services__no_overwrite_provided__returns_default_services( def test_get_services__overwrite_provided__returns_overwritten_services( + set_velocitas_package_dir, mock_filesystem: FakeFilesystem, ): os.environ["runtimeFilePath"] = "runtime.json" @@ -147,3 +163,52 @@ def test_get_services__overwrite_provided__returns_overwritten_services( assert all_services[0].config.image == "image-my-custom-service" mock_filesystem.reset() + + +def test_obtain_local_file_path__absolute_local_path(set_velocitas_workspace_dir): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + assert obtain_local_file_path(f"{root}/README.md") == f"{root}/README.md" + + +def test_obtain_local_file_path__relative_local_path(set_velocitas_workspace_dir): + assert obtain_local_file_path("README.md") == "README.md" + + +def test_obtain_local_file_path__absolute_local_path_not_available( + set_velocitas_workspace_dir, +): + pytest.raises( + FileNotFoundError, + obtain_local_file_path, + "/workspaces/velocitas-lib/README2.md", + ) + + +def test_obtain_local_file_path__relative_local_path_not_available( + set_velocitas_workspace_dir, +): + pytest.raises(FileNotFoundError, obtain_local_file_path, "README2.md") + + +def test_obtain_local_file_path__uri(set_velocitas_workspace_dir): + assert ( + obtain_local_file_path( + "https://raw.githubusercontent.com/eclipse-velocitas/velocitas-lib/main/README.md", + "/workspaces/velocitas-lib/.pytest_cache/README.md", + ) + == "/workspaces/velocitas-lib/.pytest_cache/README.md" + ) + Path.unlink(Path("/workspaces/velocitas-lib/.pytest_cache/README.md")) + + +def test_obtain_local_file_path__uri_no_download_path( + set_velocitas_cache_dir, + mock_filesystem: FakeFilesystem, +): + mock_filesystem.create_dir(get_project_cache_dir()) + assert ( + obtain_local_file_path( + "https://raw.githubusercontent.com/eclipse-velocitas/velocitas-lib/main/README.md" + ) + == "/test/cache/downloads/README.md" + ) diff --git a/velocitas_lib/__init__.py b/velocitas_lib/__init__.py index c0f45aa..a3f13c8 100644 --- a/velocitas_lib/__init__.py +++ b/velocitas_lib/__init__.py @@ -15,11 +15,11 @@ import json import os import sys +import re +import requests from io import TextIOWrapper from typing import Any, Callable, Dict, List, Optional -import requests - def to_camel_case(snake_str: str) -> str: """Return a camel case version of a snake case string. @@ -222,3 +222,50 @@ def download_file(uri: str, local_file_path: str) -> None: with open(local_file_path, "wb") as outfile: for chunk in infile.iter_content(chunk_size=8192): outfile.write(chunk) + + +def is_uri(path: str) -> bool: + """Check if the provided path is a URI. + + Args: + path (str): The path to check. + + Returns: + bool: True if the path is a URI. False otherwise. + """ + return re.match(r"(\w+)\:\/\/(\w+)", path) is not None + + +def obtain_local_file_path( + path_or_uri: str, download_path: Optional[str] = None +) -> str: + """Return the absolute path to the file, specified by a absolute/relative local path or with an URI. + + Args: + path_or_uri (str): Absolute/relative local path or URI. + download_path (str): The path to download the file. + + Returns: + str: The absolute path to the file. + """ + if not is_uri(path_or_uri): + if os.path.isfile(path_or_uri): + return path_or_uri + elif os.path.isfile(os.path.join(get_workspace_dir(), path_or_uri)): + return os.path.join(get_workspace_dir(), path_or_uri) + else: + raise FileNotFoundError(f"File {path_or_uri} not found!") + + if download_path is None: + download_path = os.path.join( + get_project_cache_dir(), "downloads", path_or_uri.split("/")[-1] + ) + if os.path.isfile(download_path): + path, file = os.path.split(download_path) + parts = file.split(".", 1) + filename = f"{parts[0]}_1.{parts[1]}" if len(parts) > 1 else f"{parts[0]}_1" + + download_path = os.path.join(path, filename) + + download_file(path_or_uri, download_path) + return download_path