sc-foundation-services 3.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sc_foundation_services-3.0.2/PKG-INFO +47 -0
- sc_foundation_services-3.0.2/README.md +14 -0
- sc_foundation_services-3.0.2/pyproject.toml +64 -0
- sc_foundation_services-3.0.2/setup.cfg +4 -0
- sc_foundation_services-3.0.2/src/sc_foundation/__init__.py +14 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_common.py +349 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_config_mgr.py +280 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_csv_reader.py +500 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_date_helper.py +877 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_json_encoder.py +286 -0
- sc_foundation_services-3.0.2/src/sc_foundation/sc_logging.py +510 -0
- sc_foundation_services-3.0.2/src/sc_foundation/validation_schema.py +28 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/PKG-INFO +47 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/SOURCES.txt +22 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/dependency_links.txt +1 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/not-zip-safe +1 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/requires.txt +27 -0
- sc_foundation_services-3.0.2/src/sc_foundation_services.egg-info/top_level.txt +1 -0
- sc_foundation_services-3.0.2/tests/test_sc_common.py +216 -0
- sc_foundation_services-3.0.2/tests/test_sc_config_mgr.py +81 -0
- sc_foundation_services-3.0.2/tests/test_sc_csv_reader.py +160 -0
- sc_foundation_services-3.0.2/tests/test_sc_date_helper.py +302 -0
- sc_foundation_services-3.0.2/tests/test_sc_json_encoder.py +43 -0
- sc_foundation_services-3.0.2/tests/test_sc_logging.py +86 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sc-foundation-services
|
|
3
|
+
Version: 3.0.2
|
|
4
|
+
Summary: Spello Consulting foundation package. A Python library for log file management; config file management; CSV and JSON file operations and more.
|
|
5
|
+
Project-URL: Homepage, https://github.com/NickElseySpelloC
|
|
6
|
+
Project-URL: Repository, https://github.com/NickElseySpelloC/sc-foundation
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: astral>=3.2
|
|
10
|
+
Requires-Dist: cerberus>=1.3.8
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Requires-Dist: mergedeep>=1.3.4
|
|
13
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
14
|
+
Requires-Dist: pytz>=2026.1.post1
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
16
|
+
Requires-Dist: timezonefinder>=8.2.2
|
|
17
|
+
Requires-Dist: tzdata>=2026.1
|
|
18
|
+
Requires-Dist: validators>=0.35.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
|
|
22
|
+
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-dotenv>=0.5.2; extra == "dev"
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocs-include-markdown-plugin>=6.2.2; extra == "docs"
|
|
27
|
+
Requires-Dist: mkdocs-material>=9.6.14; extra == "docs"
|
|
28
|
+
Requires-Dist: mkdocstrings>=0.26.1; extra == "docs"
|
|
29
|
+
Requires-Dist: mkdocstrings-python>=1.11.1; extra == "docs"
|
|
30
|
+
Requires-Dist: pdoc>=14.7.0; extra == "docs"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: sc-foundation[dev,docs]; extra == "all"
|
|
33
|
+
|
|
34
|
+
# Spello Consulting Foundation Library
|
|
35
|
+
|
|
36
|
+
A Python utility library for log file management and YAML configuration file management.
|
|
37
|
+
|
|
38
|
+
Please see the [GitHub pages](https://nickelseyspelloc.github.io/sc-foundation/) for complete documentation.
|
|
39
|
+
|
|
40
|
+
## Development Environment
|
|
41
|
+
|
|
42
|
+
Note: If making changes to the sc-foundation library, use this command to sync in all the library dependencies including unit test and documentation tools:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv sync --extra all
|
|
46
|
+
source .venv/bin/activate
|
|
47
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Spello Consulting Foundation Library
|
|
2
|
+
|
|
3
|
+
A Python utility library for log file management and YAML configuration file management.
|
|
4
|
+
|
|
5
|
+
Please see the [GitHub pages](https://nickelseyspelloc.github.io/sc-foundation/) for complete documentation.
|
|
6
|
+
|
|
7
|
+
## Development Environment
|
|
8
|
+
|
|
9
|
+
Note: If making changes to the sc-foundation library, use this command to sync in all the library dependencies including unit test and documentation tools:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync --extra all
|
|
13
|
+
source .venv/bin/activate
|
|
14
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sc-foundation-services"
|
|
3
|
+
version = "3.0.2"
|
|
4
|
+
description = "Spello Consulting foundation package. A Python library for log file management; config file management; CSV and JSON file operations and more."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"astral>=3.2",
|
|
9
|
+
"cerberus>=1.3.8",
|
|
10
|
+
"httpx>=0.28.1",
|
|
11
|
+
"mergedeep>=1.3.4",
|
|
12
|
+
"python-dateutil>=2.9.0.post0",
|
|
13
|
+
"pytz>=2026.1.post1",
|
|
14
|
+
"pyyaml>=6.0.3",
|
|
15
|
+
"timezonefinder>=8.2.2",
|
|
16
|
+
"tzdata>=2026.1",
|
|
17
|
+
"validators>=0.35.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.3.5",
|
|
27
|
+
"pytest-mock>=3.15.1",
|
|
28
|
+
"pre-commit>=3.5.0",
|
|
29
|
+
"pytest-dotenv>=0.5.2",
|
|
30
|
+
]
|
|
31
|
+
docs = [
|
|
32
|
+
"mkdocs>=1.6.1,<2.0.0",
|
|
33
|
+
"mkdocs-include-markdown-plugin>=6.2.2",
|
|
34
|
+
"mkdocs-material>=9.6.14",
|
|
35
|
+
"mkdocstrings>=0.26.1",
|
|
36
|
+
"mkdocstrings-python>=1.11.1",
|
|
37
|
+
"pdoc>=14.7.0",
|
|
38
|
+
]
|
|
39
|
+
all = [
|
|
40
|
+
"sc-foundation[dev,docs]",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/NickElseySpelloC"
|
|
45
|
+
Repository = "https://github.com/NickElseySpelloC/sc-foundation"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
package-dir = { "" = "src" }
|
|
49
|
+
include-package-data = true
|
|
50
|
+
zip-safe = false
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[dev.env.sync]
|
|
56
|
+
remote="~/Library/CloudStorage/Dropbox/Development/dev_setup/app_config/sc-foundation"
|
|
57
|
+
patterns = [
|
|
58
|
+
".env",
|
|
59
|
+
".vscode/launch.json",
|
|
60
|
+
"development/*.yaml",
|
|
61
|
+
"dev_testdevelopmenting/*.log",
|
|
62
|
+
"development/*.json",
|
|
63
|
+
"tests/config.yaml",
|
|
64
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sc-foundation package.
|
|
3
|
+
|
|
4
|
+
This package provides functions and classes for the Spello Consulting Foundation package.
|
|
5
|
+
"""
|
|
6
|
+
from .sc_common import SCCommon
|
|
7
|
+
from .sc_config_mgr import SCConfigManager
|
|
8
|
+
from .sc_csv_reader import CSVReader
|
|
9
|
+
from .sc_date_helper import DateHelper
|
|
10
|
+
from .sc_json_encoder import JSONEncoder
|
|
11
|
+
from .sc_logging import SCLogger
|
|
12
|
+
from .validation_schema import yaml_config_validation
|
|
13
|
+
|
|
14
|
+
__all__ = ["CSVReader", "DateHelper", "JSONEncoder", "SCCommon", "SCConfigManager", "SCLogger", "yaml_config_validation"]
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Common functions and classes used by other classes in the sc_foundation package."""
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import subprocess # noqa: S404
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import validators
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SCCommon:
|
|
15
|
+
"""Common functions and classes used by other classes in the sc_foundation package."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def is_valid_hostname(target: str) -> bool:
|
|
19
|
+
"""Return whether target is a valid IPv4, IPv6, or DNS hostname.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
target: The target string to validate.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A boolean indicating validity.
|
|
26
|
+
"""
|
|
27
|
+
result, _ = SCCommon.check_hostname_and_type(target)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def check_hostname_and_type(target: str) -> tuple[bool, str | None]:
|
|
32
|
+
"""Return whether target is a valid IPv4, IPv6, or DNS hostname. Also returns the type.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
target: The target string to validate.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A tuple containing a boolean indicating validity and a string indicating the type ('ipv4', 'ipv6', or 'hostname').
|
|
39
|
+
"""
|
|
40
|
+
# Make sure the target is a string
|
|
41
|
+
if not isinstance(target, str):
|
|
42
|
+
return False, None
|
|
43
|
+
|
|
44
|
+
# Check strict IPv4
|
|
45
|
+
try:
|
|
46
|
+
ipaddress.IPv4Address(target)
|
|
47
|
+
except ValueError:
|
|
48
|
+
pass
|
|
49
|
+
else:
|
|
50
|
+
if target.count(".") == 3:
|
|
51
|
+
return True, "ipv4"
|
|
52
|
+
|
|
53
|
+
# Check strict IPv6
|
|
54
|
+
try:
|
|
55
|
+
ipaddress.IPv6Address(target)
|
|
56
|
+
except ValueError:
|
|
57
|
+
pass
|
|
58
|
+
else:
|
|
59
|
+
# If it is a valid IPv6 address, return True
|
|
60
|
+
return True, "ipv6"
|
|
61
|
+
|
|
62
|
+
# Reject if it looks like a malformed IP (like 192.168.1 or 256.1.1.1)
|
|
63
|
+
if re.fullmatch(r"[0-9.]+", target):
|
|
64
|
+
return False, None
|
|
65
|
+
|
|
66
|
+
# Validate hostname using validators library
|
|
67
|
+
if validators.domain(target) or validators.hostname(target, rfc_1034=True):
|
|
68
|
+
return True, "hostname"
|
|
69
|
+
|
|
70
|
+
return False, None
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def ping_host(ip_address: str, timeout: int = 1) -> bool:
|
|
74
|
+
"""Pings an IP address and returns True if the host is responding, False otherwise.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ip_address: The IP address to ping.
|
|
78
|
+
timeout: Timeout in seconds for the ping response. Default is 1 second.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
RuntimeError: If the IP address is invalid or the ping system call fails.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
result (bool): True if the host responds, False otherwise.
|
|
85
|
+
"""
|
|
86
|
+
# Determine the ping command based on the operating system
|
|
87
|
+
param = "-n" if platform.system().lower() == "windows" else "-c"
|
|
88
|
+
|
|
89
|
+
if not SCCommon.is_valid_hostname(ip_address):
|
|
90
|
+
error_msg = f"Invalid IP address: {ip_address}"
|
|
91
|
+
raise RuntimeError(error_msg)
|
|
92
|
+
|
|
93
|
+
command = ["ping", param, "1", "-W", str(timeout), ip_address]
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Run the ping command using subprocess for better security
|
|
97
|
+
result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=False, check=False) # noqa: S603
|
|
98
|
+
response_code = result.returncode
|
|
99
|
+
except OSError as e:
|
|
100
|
+
error_msg = f"Error pinging {ip_address}: {e}"
|
|
101
|
+
raise RuntimeError(error_msg) from e
|
|
102
|
+
else:
|
|
103
|
+
# Return True if the ping was successful (exit code 0)
|
|
104
|
+
return response_code == 0
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def check_internet_connection(urls=None, timeout: int = 3) -> bool:
|
|
108
|
+
"""Check if the system has an active internet connection by trying to open a connection to common websites.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
urls (list): A list of URLs to check for internet connectivity. Defaults to common DNS servers and websites.
|
|
112
|
+
timeout (int): The timeout in seconds for each request.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if the system is connected to the internet, False otherwise.
|
|
116
|
+
"""
|
|
117
|
+
if urls is None:
|
|
118
|
+
urls = [
|
|
119
|
+
"https://1.1.1.1", # Cloudflare DNS
|
|
120
|
+
"https://8.8.8.8", # Google DNS
|
|
121
|
+
"https://www.google.com",
|
|
122
|
+
"https://www.cloudflare.com"
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
for url in urls:
|
|
126
|
+
try:
|
|
127
|
+
response = httpx.get(url, timeout=timeout, follow_redirects=True)
|
|
128
|
+
if response.status_code < 400:
|
|
129
|
+
return True
|
|
130
|
+
except httpx.RequestError:
|
|
131
|
+
continue
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def get_os() -> str:
|
|
136
|
+
"""Return the name of the operating system.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The name of the operating system in lowercase.
|
|
140
|
+
"""
|
|
141
|
+
# Get the platform name and convert it to lowercase
|
|
142
|
+
platform_name = platform.system().lower()
|
|
143
|
+
|
|
144
|
+
if platform_name == "darwin":
|
|
145
|
+
platform_name = "macos"
|
|
146
|
+
|
|
147
|
+
return platform_name
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def is_probable_path(possible_path: str | Path) -> bool:
|
|
151
|
+
"""Check if the given string or Path object is likely to be a file path.
|
|
152
|
+
|
|
153
|
+
This method checks if the string is an absolute path, contains a path separator, or has a file extension.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
possible_path: The string to check.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if the string is likely a file path, False otherwise.
|
|
160
|
+
"""
|
|
161
|
+
max_path = 260 if SCCommon.get_os() == "windows" else os.pathconf("/", "PC_PATH_MAX")
|
|
162
|
+
|
|
163
|
+
path_obj = None
|
|
164
|
+
if isinstance(possible_path, Path):
|
|
165
|
+
path_str = str(possible_path)
|
|
166
|
+
path_obj = possible_path
|
|
167
|
+
else:
|
|
168
|
+
path_str = possible_path
|
|
169
|
+
|
|
170
|
+
if len(path_str) > max_path:
|
|
171
|
+
# If the path is longer than the maximum allowed path length, it cannot be a valid path
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if path_obj is None:
|
|
175
|
+
path_obj = Path(possible_path)
|
|
176
|
+
|
|
177
|
+
# Check if it's absolute, or contains a path separator, or has a file extension
|
|
178
|
+
if path_obj.is_absolute():
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
if "/" in path_str or "\\" in path_str:
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
# Check if the path has a file extension
|
|
185
|
+
return bool(path_obj.suffix and path_obj.suffix.lower() is not None)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def get_project_root(marker_files=("pyproject.toml", ".project_root", "uv.lock", ".git")) -> Path:
|
|
189
|
+
"""Return the root folder of the Python project.
|
|
190
|
+
|
|
191
|
+
By default, this function looks for marker files like pyproject.toml, .project_root, uv.lock, or .git to
|
|
192
|
+
identify the project root. It starts from the directory of this file and walks upwards until it finds one
|
|
193
|
+
of the marker files. If it cannot find any of the marker files, it raises a RuntimeError.
|
|
194
|
+
|
|
195
|
+
If the environment variable SC_FOUNDATION_PROJECT_ROOT is set, it will check if that path exists and is a directory,
|
|
196
|
+
and return it as the project root if so. This allows users to override the automatic detection of the project
|
|
197
|
+
root if needed (e.g., if they have an unusual project structure or want to use the foundation in a different project
|
|
198
|
+
without copying this file).
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
marker_files (tuple): A tuple of file names that indicate the project root.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
RuntimeError: If the project root cannot be found.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
root_dir (Path): The root folder of the Python project as a Path object.
|
|
208
|
+
"""
|
|
209
|
+
path = None
|
|
210
|
+
env_path = os.environ.get("SC_FOUNDATION_PROJECT_ROOT") # Issue 32
|
|
211
|
+
if env_path:
|
|
212
|
+
path = Path(env_path).resolve()
|
|
213
|
+
if path and path.exists() and path.is_dir():
|
|
214
|
+
return path
|
|
215
|
+
|
|
216
|
+
# Default behaviour is to look for the project root based on the location of this file and the presence of marker files. This allows the foundation to be used in other projects without requiring users
|
|
217
|
+
path = Path(__file__).resolve()
|
|
218
|
+
|
|
219
|
+
# Walk upwards until we find a marker file
|
|
220
|
+
for parent in [path, *list(path.parents)]:
|
|
221
|
+
for marker in marker_files:
|
|
222
|
+
if (parent / marker).exists():
|
|
223
|
+
return parent
|
|
224
|
+
|
|
225
|
+
error_msg = f"Project root not found. Looked for markers: {marker_files}"
|
|
226
|
+
if env_path:
|
|
227
|
+
error_msg += f" (also checked SC_FOUNDATION_PROJECT_ROOT={env_path})"
|
|
228
|
+
raise RuntimeError(error_msg)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def select_file_location(file_name: str, create_folder: bool = False) -> Path | None:
|
|
232
|
+
"""Select the file location for the given file name. It resolves an absolute path for the file_name as follows.
|
|
233
|
+
|
|
234
|
+
1. If file_name is an absolute path, return it as a Path object.
|
|
235
|
+
2. If file_name is a relative path (contains parent directories), return the absolute path based on the current working directory.
|
|
236
|
+
3. If file_name is just a file name, look for it in the current working directory first, then in the root directory.
|
|
237
|
+
|
|
238
|
+
The root directly is defined as the directory containing the main script being executed (the module containing __main__).
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
RuntimeError: If the project root cannot be determined.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
file_name: The name of the file to locate. Can be just a file name, or a relative or absolute path.
|
|
245
|
+
create_folder: If True, create the parent folder if it does not exist. Default is False.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
file_path (Path): The full path to the file as a Path object. None if the file_name does not appear to be a path.
|
|
249
|
+
"""
|
|
250
|
+
return_file_path = None
|
|
251
|
+
|
|
252
|
+
# Look at the file_name and see if it looks like a path
|
|
253
|
+
if not SCCommon.is_probable_path(file_name):
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
# Check to see if file_name is a full path or just a file name
|
|
257
|
+
return_file_path = Path(file_name)
|
|
258
|
+
|
|
259
|
+
# Check if file_name is an absolute path, return this even if it does not exist
|
|
260
|
+
if return_file_path.is_absolute():
|
|
261
|
+
SCCommon._create_folder_if_not_exists(return_file_path.parent) if create_folder else None
|
|
262
|
+
return return_file_path
|
|
263
|
+
|
|
264
|
+
# Check if file_name contains any parent directories (i.e., is a relative path)
|
|
265
|
+
# If so, return this even if it does not exist
|
|
266
|
+
if return_file_path.parent != Path("."): # noqa: PTH201
|
|
267
|
+
# It's a relative path
|
|
268
|
+
return_file_path = (Path.cwd() / return_file_path).resolve()
|
|
269
|
+
SCCommon._create_folder_if_not_exists(return_file_path.parent) if create_folder else None
|
|
270
|
+
return return_file_path
|
|
271
|
+
|
|
272
|
+
# Otherwise, assume it's just a file name and look for it in the current directory and the script directory
|
|
273
|
+
current_dir = Path.cwd()
|
|
274
|
+
return_file_path = current_dir / file_name
|
|
275
|
+
if not return_file_path.exists():
|
|
276
|
+
try:
|
|
277
|
+
project_root_dir = SCCommon.get_project_root()
|
|
278
|
+
return_file_path = project_root_dir / file_name
|
|
279
|
+
except RuntimeError as e:
|
|
280
|
+
error_msg = f"Cannot determine project root to locate file '{file_name}': {e}"
|
|
281
|
+
raise RuntimeError(error_msg) from e
|
|
282
|
+
|
|
283
|
+
if return_file_path:
|
|
284
|
+
SCCommon._create_folder_if_not_exists(return_file_path.parent) if create_folder else None
|
|
285
|
+
|
|
286
|
+
return return_file_path
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def select_folder_location(folder_path: str | None = None, create_folder: bool = False) -> Path | None:
|
|
290
|
+
"""Return an absolute folder path for the given (relative) folder path.
|
|
291
|
+
|
|
292
|
+
If folder_path is None, return the project root folder.
|
|
293
|
+
If folder_path is an absolute path, return it as a Path object.
|
|
294
|
+
If folder_path is a relative path, return the absolute path based on the project root directory.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
folder_path: The folder path to locate. Can be None, or a relative or absolute path.
|
|
298
|
+
create_folder: If True, create the folder if it does not exist. Default is False.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
RuntimeError: If the project root cannot be determined or if folder creation fails.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The full path to the folder as a Path object. None if folder_path is None and project root cannot be determined.
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
project_root = SCCommon.get_project_root()
|
|
308
|
+
except RuntimeError as e:
|
|
309
|
+
raise RuntimeError(e) from e
|
|
310
|
+
|
|
311
|
+
if folder_path is None:
|
|
312
|
+
return project_root
|
|
313
|
+
|
|
314
|
+
selected_folder = Path(folder_path)
|
|
315
|
+
|
|
316
|
+
# Check if folder_path is an absolute path, return this even if it does not exist
|
|
317
|
+
if not selected_folder.is_absolute():
|
|
318
|
+
selected_folder = (project_root / selected_folder).resolve()
|
|
319
|
+
|
|
320
|
+
if create_folder:
|
|
321
|
+
SCCommon._create_folder_if_not_exists(selected_folder)
|
|
322
|
+
|
|
323
|
+
return selected_folder
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def get_process_id() -> int:
|
|
327
|
+
"""Return the process ID of the current process.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The process ID of the current process.
|
|
331
|
+
"""
|
|
332
|
+
return os.getpid()
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _create_folder_if_not_exists(folder_path: Path) -> None:
|
|
336
|
+
"""Create the folder if it does not exist.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
folder_path: The path of the folder to create.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
RuntimeError: If folder creation fails.
|
|
343
|
+
"""
|
|
344
|
+
if not folder_path.exists():
|
|
345
|
+
try:
|
|
346
|
+
folder_path.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
except OSError as e:
|
|
348
|
+
error_msg = f"Error creating folder '{folder_path}': {e}"
|
|
349
|
+
raise RuntimeError(error_msg) from e
|