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