mirror of
https://github.com/rendercv/rendercv.git
synced 2026-10-24 21:28:47 -04:00
216 lines
7.3 KiB
Python
216 lines
7.3 KiB
Python
"""
|
|
The `rendercv.cli.utilities` module contains utility functions that are required by CLI.
|
|
"""
|
|
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import shutil
|
|
import urllib.request
|
|
from typing import Optional
|
|
|
|
import typer
|
|
import pydantic
|
|
|
|
|
|
def set_or_update_a_value(
|
|
data_model: pydantic.BaseModel | dict | list,
|
|
key: str,
|
|
value: str,
|
|
sub_model: pydantic.BaseModel | dict | list = None,
|
|
):
|
|
"""Set or update a value in a data model for a specific key. For example, a key can
|
|
be `cv.sections.education.3.institution` and the value can be "Bogazici University".
|
|
|
|
Args:
|
|
data_model (pydantic.BaseModel | dict | list): The data model to set or update
|
|
the value.
|
|
key (str): The key to set or update the value.
|
|
value (Any): The value to set or update.
|
|
sub_model (pydantic.BaseModel | dict | list, optional): The sub model to set or
|
|
update the value. This is used for recursive calls. When the value is set
|
|
to a sub model, the original data model is validated. Defaults to None.
|
|
"""
|
|
# Recursively call this function until the last key is reached:
|
|
|
|
# Rename `sections` with `sections_input` since the key is `sections` is an alias:
|
|
key = key.replace("sections.", "sections_input.")
|
|
keys = key.split(".")
|
|
|
|
if sub_model is not None:
|
|
model = sub_model
|
|
else:
|
|
model = data_model
|
|
|
|
if len(keys) == 1:
|
|
# Set the value:
|
|
if value.startswith("{") and value.endswith("}"):
|
|
# Allow users to assign dictionaries:
|
|
value = eval(value)
|
|
elif value.startswith("[") and value.endswith("]"):
|
|
# Allow users to assign lists:
|
|
value = eval(value)
|
|
|
|
if isinstance(model, pydantic.BaseModel):
|
|
setattr(model, key, value)
|
|
elif isinstance(model, dict):
|
|
model[key] = value
|
|
elif isinstance(model, list):
|
|
model[int(key)] = value
|
|
else:
|
|
raise ValueError(
|
|
"The data model should be either a Pydantic data model, dictionary, or"
|
|
" list.",
|
|
)
|
|
|
|
data_model = type(data_model).model_validate(
|
|
(data_model.model_dump(by_alias=True))
|
|
)
|
|
return data_model
|
|
else:
|
|
# get the first key and call the function with remaining keys:
|
|
first_key = keys[0]
|
|
key = ".".join(keys[1:])
|
|
if isinstance(model, pydantic.BaseModel):
|
|
sub_model = getattr(model, first_key)
|
|
elif isinstance(model, dict):
|
|
sub_model = model[first_key]
|
|
elif isinstance(model, list):
|
|
sub_model = model[int(first_key)]
|
|
else:
|
|
raise ValueError(
|
|
"The data model should be either a Pydantic data model, dictionary, or"
|
|
" list.",
|
|
)
|
|
|
|
set_or_update_a_value(data_model, key, value, sub_model)
|
|
|
|
|
|
def get_latest_version_number_from_pypi() -> Optional[str]:
|
|
"""Get the latest version number of RenderCV from PyPI.
|
|
|
|
Example:
|
|
```python
|
|
get_latest_version_number_from_pypi()
|
|
```
|
|
returns
|
|
`#!python "1.1"`
|
|
|
|
Returns:
|
|
Optional[str]: The latest version number of RenderCV from PyPI. Returns None if
|
|
the version number cannot be fetched.
|
|
"""
|
|
version = None
|
|
url = "https://pypi.org/pypi/rendercv/json"
|
|
try:
|
|
with urllib.request.urlopen(url) as response:
|
|
data = response.read()
|
|
encoding = response.info().get_content_charset("utf-8")
|
|
json_data = json.loads(data.decode(encoding))
|
|
version = json_data["info"]["version"]
|
|
except Exception:
|
|
pass
|
|
|
|
return version
|
|
|
|
|
|
def get_error_message_and_location_and_value_from_a_custom_error(
|
|
error_string: str,
|
|
) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
|
"""Look at a string and figure out if it's a custom error message that has been
|
|
sent from [`data_models.py`](data_models.md). If it is, then return the custom
|
|
message, location, and the input value.
|
|
|
|
This is done because sometimes we raise an error about a specific field in the model
|
|
validation level, but Pydantic doesn't give us the exact location of the error
|
|
because it's a model-level error. So, we raise a custom error with three string
|
|
arguments: message, location, and input value. Those arguments then combined into a
|
|
string by Python. This function is used to parse that custom error message and
|
|
return the three values.
|
|
|
|
Args:
|
|
error_string (str): The error message.
|
|
Returns:
|
|
tuple[Optional[str], Optional[str], Optional[str]]: The custom message,
|
|
location, and the input value.
|
|
"""
|
|
pattern = r"""\(['"](.*)['"], '(.*)', '(.*)'\)"""
|
|
match = re.search(pattern, error_string)
|
|
if match:
|
|
return match.group(1), match.group(2), match.group(3)
|
|
else:
|
|
return None, None, None
|
|
|
|
|
|
def copy_templates(
|
|
folder_name: str,
|
|
copy_to: pathlib.Path,
|
|
new_folder_name: Optional[str] = None,
|
|
) -> Optional[pathlib.Path]:
|
|
"""Copy one of the folders found in `rendercv.templates` to `copy_to`.
|
|
|
|
Args:
|
|
folder_name (str): The name of the folder to be copied.
|
|
copy_to (pathlib.Path): The path to copy the folder to.
|
|
Returns:
|
|
Optional[pathlib.Path]: The path to the copied folder.
|
|
"""
|
|
# copy the package's theme files to the current directory
|
|
template_directory = pathlib.Path(__file__).parent / "themes" / folder_name
|
|
if new_folder_name:
|
|
destination = copy_to / new_folder_name
|
|
else:
|
|
destination = copy_to / folder_name
|
|
|
|
if destination.exists():
|
|
return None
|
|
else:
|
|
# copy the folder but don't include __init__.py:
|
|
shutil.copytree(
|
|
template_directory,
|
|
destination,
|
|
ignore=shutil.ignore_patterns("__init__.py", "__pycache__"),
|
|
)
|
|
|
|
return destination
|
|
|
|
|
|
def parse_render_command_override_arguments(
|
|
extra_arguments: typer.Context,
|
|
) -> dict["str", "str"]:
|
|
"""Parse extra arguments given to the `render` command as data model key and value
|
|
pairs and return them as a dictionary.
|
|
|
|
Args:
|
|
extra_arguments (typer.Context): The extra arguments context.
|
|
Returns:
|
|
dict["str", "str"]: The key and value pairs.
|
|
"""
|
|
key_and_values: dict["str", "str"] = dict()
|
|
|
|
# `extra_arguments.args` is a list of arbitrary arguments that haven't been
|
|
# specified in `cli_render_command` function's definition. They are used to allow
|
|
# users to edit their data model in CLI. The elements with even indexes in this list
|
|
# are keys that start with double dashed, such as
|
|
# `--cv.sections.education.0.institution`. The following elements are the
|
|
# corresponding values of the key, such as `"Bogazici University"`. The for loop
|
|
# below parses `ctx.args` accordingly.
|
|
|
|
if len(extra_arguments.args) % 2 != 0:
|
|
raise ValueError(
|
|
"There is a problem with the extra arguments! Each key should have"
|
|
" a corresponding value."
|
|
)
|
|
|
|
for i in range(0, len(extra_arguments.args), 2):
|
|
key = extra_arguments.args[i]
|
|
value = extra_arguments.args[i + 1]
|
|
if not key.startswith("--"):
|
|
raise ValueError(f"The key ({key}) should start with double dashes!")
|
|
|
|
key = key.replace("--", "")
|
|
|
|
key_and_values[key] = value
|
|
|
|
return key_and_values
|