This commit is contained in:
Sina Atalay
2025-12-22 16:54:22 +03:00
parent 17114e03b2
commit b524e377fe
32 changed files with 180 additions and 173 deletions

View File

@@ -1,12 +1,20 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.8 # Ruff version.
rev: v0.14.10
hooks: hooks:
- id: ruff # Run the linter.
# - repo: https://github.com/RobertCraigie/pyright-python - id: ruff-check
# rev: v1.1.407 # Run the formatter.
# hooks: - id: ruff-format
# - id: pyright - repo: local
hooks:
- id: ty-check
name: ty-check
language: python
entry: ty check src tests
pass_filenames: false
additional_dependencies: [ty]
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.4.1
hooks: hooks:

View File

@@ -99,7 +99,10 @@ def define_env(env):
] ]
# Available themes strings (put available themes between ``) # Available themes strings (put available themes between ``)
themes = [f"`{theme}`" if theme != "classic" else "`classic` (default)" for theme in available_themes] themes = [
f"`{theme}`" if theme != "classic" else "`classic` (default)"
for theme in available_themes
]
env.variables["available_themes"] = ", ".join(themes) env.variables["available_themes"] = ", ".join(themes)
env.variables["theme_count"] = len(available_themes) env.variables["theme_count"] = len(available_themes)

View File

@@ -17,7 +17,7 @@ format-file target:
check: check:
uv run --frozen --all-extras ruff check src tests uv run --frozen --all-extras ruff check src tests
uv run --frozen --all-extras pyright src tests uv run --frozen --all-extras ty check src tests
uv run --frozen --all-extras pre-commit run --all-files uv run --frozen --all-extras pre-commit run --all-files
# Testing: # Testing:

View File

@@ -89,9 +89,9 @@ rendercv = 'rendercv.cli.entry_point:entry_point'
# Virtual Environment Dependencies: # Virtual Environment Dependencies:
[dependency-groups] [dependency-groups]
dev = [ dev = [
'ruff>=0.14.8', # Lint and format the code 'ruff>=0.14.10', # Lint and format the code
'black>=25.12.0', # Format the code 'black>=25.12.0', # Format the code
'pyright>=1.1.407', # Type checking 'ty>=0.0.5', # Type checking
'pre-commit>=4.5.0', # Run checks before committing 'pre-commit>=4.5.0', # Run checks before committing
'pytest>=9.0.2', # Run tests 'pytest>=9.0.2', # Run tests
'pytest-cov>=7.0.0', # Coverage plugin for pytest with xdist support 'pytest-cov>=7.0.0', # Coverage plugin for pytest with xdist support
@@ -170,17 +170,8 @@ enable-unstable-feature = [
'string_processing', 'string_processing',
] # Break strings into multiple lines ] # Break strings into multiple lines
[tool.pyright] [tool.ty.rules]
venvPath = "." unused-ignore-comment = "error"
venv = ".venv"
typeCheckingMode = 'standard'
enableTypeIgnoreComments = false
reportUnnecessaryTypeIgnoreComment = true
reportIncompatibleVariableOverride = false
reportIncompatibleMethodOverride = false
reportMissingTypeStubs = false
reportPrivateUsage = false
exclude = ['docs/**/*']
[tool.coverage.run] [tool.coverage.run]
source = ['src/rendercv'] # Measure coverage in this source source = ['src/rendercv'] # Measure coverage in this source

View File

@@ -29,7 +29,7 @@ def pdf_to_png(pdf_file_path: pathlib.Path) -> list[pathlib.Path]:
png_files = [] png_files = []
pdf = fitz.open(pdf_file_path) # open the PDF file pdf = fitz.open(pdf_file_path) # open the PDF file
for page in pdf: # iterate the pages for page in pdf: # iterate the pages
image = page.get_pixmap(dpi=300) # type: ignore image = page.get_pixmap(dpi=300)
assert page.number is not None assert page.number is not None
png_file_path = png_directory / f"{png_file_name}_{page.number + 1}.png" png_file_path = png_directory / f"{png_file_name}_{page.number + 1}.png"
image.save(png_file_path) image.save(png_file_path)

View File

@@ -184,7 +184,7 @@ def cli_command_render(
' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].', ' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].',
), ),
] = None, ] = None,
extra_data_model_override_arguments: typer.Context = None, # pyright: ignore[reportArgumentType] extra_data_model_override_arguments: typer.Context = None, # ty: ignore[invalid-parameter-default]
): ):
arguments: BuildRendercvModelArguments = { arguments: BuildRendercvModelArguments = {
"design_file_path_or_contents": design if design else None, "design_file_path_or_contents": design if design else None,

View File

@@ -61,7 +61,7 @@ def timed_step[T, **P](
elif isinstance(result, list) and result: elif isinstance(result, list) and result:
if len(result) > 1: if len(result) > 1:
message = f"{message}s" message = f"{message}s"
paths = result paths = result # ty: ignore[invalid-assignment]
if paths: if paths:
progress_panel.update_progress( progress_panel.update_progress(

View File

@@ -27,7 +27,10 @@ def run_function_if_file_changes(file_path: pathlib.Path, function: Callable):
super().__init__() super().__init__()
self.function = function self.function = function
def on_modified(self, event: watchdog.events.FileModifiedEvent) -> None: def on_modified(
self,
event: watchdog.events.DirModifiedEvent | watchdog.events.FileModifiedEvent,
) -> None:
if event.src_path != str(file_path.absolute()): if event.src_path != str(file_path.absolute()):
return return

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
@dataclass @dataclass
class RenderCVValidationError: class RenderCVValidationError:
location: tuple[str, ...] location: tuple[str, ...]
yaml_location: tuple[tuple[int, int], tuple[int, int]] yaml_location: tuple[tuple[int, int], tuple[int, int]] | None
message: str message: str
input: str input: str

View File

@@ -70,6 +70,9 @@ def resolve_rendercv_file_path(
else None else None
), ),
} }
file_path_placeholders = {
k: v for k, v in file_path_placeholders.items() if v is not None
}
file_name = substitute_placeholders(file_path.name, file_path_placeholders) file_name = substitute_placeholders(file_path.name, file_path_placeholders)
resolved_file_path = file_path.parent / file_name resolved_file_path = file_path.parent / file_name
resolved_file_path.parent.mkdir(parents=True, exist_ok=True) resolved_file_path.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -117,7 +117,7 @@ def parse_connections(rendercv_model: RenderCVModel) -> list[Connection]:
for website in websites: for website in websites:
url = str(website) url = str(website)
body = clean_url(website) body = clean_url(url)
connections.append( connections.append(
Connection( Connection(
fontawesome_icon=fontawesome_icons[key], url=url, body=body fontawesome_icon=fontawesome_icons[key], url=url, body=body

View File

@@ -98,11 +98,11 @@ def render_entry_templates[EntryType: Entry](
) )
if "URL" in entry_fields: if "URL" in entry_fields:
entry_fields["URL"] = process_url(entry) entry_fields["URL"] = process_url(entry) # ty: ignore[invalid-argument-type]
if "DOI" in entry_fields: if "DOI" in entry_fields:
entry_fields["URL"] = process_url(entry) entry_fields["URL"] = process_url(entry) # ty: ignore[invalid-argument-type]
entry_fields["DOI"] = process_doi(entry) entry_fields["DOI"] = process_doi(entry) # ty: ignore[invalid-argument-type]
if "SUMMARY" in entry_fields: if "SUMMARY" in entry_fields:
entry_fields["SUMMARY"] = process_summary(entry_fields["SUMMARY"]) entry_fields["SUMMARY"] = process_summary(entry_fields["SUMMARY"])
@@ -268,9 +268,9 @@ def process_url(entry: Entry) -> str:
""" """
if isinstance(entry, PublicationEntry) and entry.doi: if isinstance(entry, PublicationEntry) and entry.doi:
return process_doi(entry) return process_doi(entry)
if hasattr(entry, "url") and entry.url: # pyright: ignore[reportAttributeAccessIssue] if hasattr(entry, "url") and entry.url:
url = entry.url # pyright: ignore[reportAttributeAccessIssue] url = entry.url
return f"[{clean_url(url)}]({url})" return f"[{clean_url(url)}]({url})" # ty: ignore[invalid-argument-type]
raise RenderCVInternalError("URL is not provided for this entry.") raise RenderCVInternalError("URL is not provided for this entry.")

View File

@@ -36,15 +36,15 @@ def process_model(
if file_type == "typst": if file_type == "typst":
string_processors.extend([markdown_to_typst]) string_processors.extend([markdown_to_typst])
rendercv_model.cv.plain_name = rendercv_model.cv.name # pyright: ignore[reportAttributeAccessIssue] rendercv_model.cv.plain_name = rendercv_model.cv.name # ty: ignore[unresolved-attribute]
rendercv_model.cv.name = apply_string_processors( rendercv_model.cv.name = apply_string_processors(
rendercv_model.cv.name, string_processors rendercv_model.cv.name, string_processors
) )
rendercv_model.cv.headline = apply_string_processors( rendercv_model.cv.headline = apply_string_processors(
rendercv_model.cv.headline, string_processors rendercv_model.cv.headline, string_processors
) )
rendercv_model.cv.connections = compute_connections(rendercv_model, file_type) # pyright: ignore[reportAttributeAccessIssue] rendercv_model.cv.connections = compute_connections(rendercv_model, file_type) # ty: ignore[unresolved-attribute]
rendercv_model.cv.top_note = render_top_note_template( # pyright: ignore[reportAttributeAccessIssue] rendercv_model.cv.top_note = render_top_note_template( # ty: ignore[unresolved-attribute]
rendercv_model.design.templates.top_note, rendercv_model.design.templates.top_note,
locale=rendercv_model.locale, locale=rendercv_model.locale,
current_date=rendercv_model.settings.current_date, current_date=rendercv_model.settings.current_date,
@@ -53,7 +53,7 @@ def process_model(
string_processors=string_processors, string_processors=string_processors,
) )
rendercv_model.cv.footer = render_footer_template( # pyright: ignore[reportAttributeAccessIssue] rendercv_model.cv.footer = render_footer_template( # ty: ignore[unresolved-attribute]
rendercv_model.design.templates.footer, rendercv_model.design.templates.footer,
locale=rendercv_model.locale, locale=rendercv_model.locale,
current_date=rendercv_model.settings.current_date, current_date=rendercv_model.settings.current_date,

View File

@@ -57,9 +57,9 @@ def build_keyword_matcher_pattern(keywords: frozenset[str]) -> re.Pattern:
message = "Keywords cannot be empty" message = "Keywords cannot be empty"
raise RenderCVInternalError(message) raise RenderCVInternalError(message)
pattern = ( escaped: list[str] = [re.escape(k) for k in keywords]
"(" + "|".join(sorted(map(re.escape, keywords), key=len, reverse=True)) + ")" escaped.sort(key=len, reverse=True)
) pattern = "(" + "|".join(escaped) + ")"
return re.compile(pattern) return re.compile(pattern)

View File

@@ -37,7 +37,7 @@ available_entry_models: tuple[type[EntryModel], ...] = get_args(EntryModel.__val
available_entry_type_names: tuple[str, ...] = tuple( available_entry_type_names: tuple[str, ...] = tuple(
[entry_type.__name__ for entry_type in available_entry_models] + ["TextEntry"] [entry_type.__name__ for entry_type in available_entry_models] + ["TextEntry"]
) )
type ListOfEntries = list[str] | reduce( # pyright: ignore[reportInvalidTypeForm] type ListOfEntries = list[str] | reduce( # ty: ignore[invalid-type-form]
or_, [list[entry_type] for entry_type in available_entry_models] or_, [list[entry_type] for entry_type in available_entry_models]
) )
@@ -112,8 +112,8 @@ def create_section_models(
return pydantic.create_model( return pydantic.create_model(
model_name, model_name,
entry_type=(Literal[entry_type_name], ...), entry_type=(Literal[entry_type_name], ...), # ty: ignore[invalid-type-form]
entries=(list[entry_type], ...), entries=(list[entry_type], ...), # ty: ignore[invalid-type-form]
__base__=BaseRenderCVSection, __base__=BaseRenderCVSection,
) )

View File

@@ -26,7 +26,7 @@ type SocialNetworkName = Literal[
"WhatsApp", "WhatsApp",
"Leetcode", "Leetcode",
"X", "X",
"Bluesky" "Bluesky",
] ]
available_social_networks = get_args(SocialNetworkName.__value__) available_social_networks = get_args(SocialNetworkName.__value__)
url_dictionary: dict[SocialNetworkName, str] = { url_dictionary: dict[SocialNetworkName, str] = {
@@ -44,7 +44,7 @@ url_dictionary: dict[SocialNetworkName, str] = {
"WhatsApp": "https://wa.me/", "WhatsApp": "https://wa.me/",
"Leetcode": "https://leetcode.com/u/", "Leetcode": "https://leetcode.com/u/",
"X": "https://x.com/", "X": "https://x.com/",
"Bluesky": "https://bsky.app/profile/" "Bluesky": "https://bsky.app/profile/",
} }
@@ -121,7 +121,8 @@ class SocialNetwork(BaseModelWithoutExtraKeys):
if not re.fullmatch(bluesky_username_pattern, username): if not re.fullmatch(bluesky_username_pattern, username):
raise pydantic_core.PydanticCustomError( raise pydantic_core.PydanticCustomError(
CustomPydanticErrorTypes.other.value, CustomPydanticErrorTypes.other.value,
"Bluesky username should be a valid handle with no '@' (e.g., 'username.bsky.social' or 'domain.com').", "Bluesky username should be a valid handle with no '@' (e.g.,"
" 'username.bsky.social' or 'domain.com').",
) )
case "WhatsApp": case "WhatsApp":
phone_validator = pydantic.TypeAdapter( phone_validator = pydantic.TypeAdapter(

View File

@@ -40,7 +40,7 @@ def discover_other_themes() -> list[type[ClassicTheme]]:
# Build discriminated union dynamically # Build discriminated union dynamically
type BuiltInDesign = Annotated[ type BuiltInDesign = Annotated[
ClassicTheme | reduce(or_, discover_other_themes()), # pyright: ignore[reportInvalidTypeForm] ClassicTheme | reduce(or_, discover_other_themes()), # ty: ignore[invalid-type-form]
pydantic.Field(discriminator="theme"), pydantic.Field(discriminator="theme"),
] ]
available_themes: list[str] = [ available_themes: list[str] = [

View File

@@ -50,4 +50,4 @@ available_font_families = sorted(
) )
type FontFamily = SkipJsonSchema[str] | Literal[*tuple(available_font_families)] # pyright: ignore[reportInvalidTypeForm] type FontFamily = SkipJsonSchema[str] | Literal[*tuple(available_font_families)] # ty: ignore[invalid-type-form]

View File

@@ -40,7 +40,7 @@ def discover_other_locales() -> list[type[EnglishLocale]]:
# Build discriminated union dynamically # Build discriminated union dynamically
type Locale = Annotated[ type Locale = Annotated[
EnglishLocale | reduce(or_, discover_other_locales()), # pyright: ignore[reportInvalidTypeForm] EnglishLocale | reduce(or_, discover_other_locales()), # ty: ignore[invalid-type-form]
pydantic.Field(discriminator="language"), pydantic.Field(discriminator="language"),
] ]
available_locales = [ available_locales = [

View File

@@ -1,29 +1,14 @@
import copy import copy
from typing import overload
from rendercv.exception import RenderCVUserError from rendercv.exception import RenderCVUserError
@overload def update_value_by_location[T: dict | list](
def update_value_by_location( dict_or_list: T,
dict_or_list: dict,
key: str, key: str,
value: str, value: str,
full_key: str, full_key: str,
) -> dict: ... ) -> T:
@overload
def update_value_by_location(
dict_or_list: list,
key: str,
value: str,
full_key: str,
) -> list: ...
def update_value_by_location(
dict_or_list: dict | list,
key: str,
value: str,
full_key: str,
) -> dict | list:
"""Navigate nested structure via dotted path and update value. """Navigate nested structure via dotted path and update value.
Why: Why:
@@ -89,21 +74,21 @@ def update_value_by_location(
new_value = value new_value = value
else: else:
new_value = update_value_by_location( new_value = update_value_by_location(
dict_or_list[first_key], # pyright: ignore[reportArgumentType, reportCallIssue] dict_or_list[first_key],
remaining_key, remaining_key,
value, value,
full_key=full_key, full_key=full_key,
) )
dict_or_list[first_key] = new_value # pyright: ignore[reportArgumentType, reportCallIssue] dict_or_list[first_key] = new_value
return dict_or_list return dict_or_list
def apply_overrides_to_dictionary( def apply_overrides_to_dictionary[T: dict](
dictionary: dict, dictionary: T,
overrides: dict[str, str], overrides: dict[str, str],
) -> dict: ) -> T:
"""Apply multiple CLI overrides to dictionary. """Apply multiple CLI overrides to dictionary.
Why: Why:

View File

@@ -1,9 +1,8 @@
import pathlib import pathlib
from typing import cast from typing import Any, cast
import pydantic import pydantic
import pydantic_core import pydantic_core
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap from ruamel.yaml.comments import CommentedMap
from rendercv.exception import RenderCVInternalError, RenderCVValidationError from rendercv.exception import RenderCVInternalError, RenderCVValidationError
@@ -27,7 +26,8 @@ unwanted_locations = (
def parse_plain_pydantic_error( def parse_plain_pydantic_error(
plain_error: pydantic_core.ErrorDetails, user_input_as_commented_map: CommentedMap plain_error: pydantic_core.ErrorDetails,
input_dictionary: CommentedMap | dict[str, Any],
) -> RenderCVValidationError: ) -> RenderCVValidationError:
"""Transform raw Pydantic error into user-friendly validation error with YAML coordinates. """Transform raw Pydantic error into user-friendly validation error with YAML coordinates.
@@ -86,16 +86,20 @@ def parse_plain_pydantic_error(
if not isinstance(plain_error["input"], dict | list) if not isinstance(plain_error["input"], dict | list)
else "..." else "..."
), ),
yaml_location=get_coordinates_of_a_key_in_a_yaml_object( yaml_location=(
user_input_as_commented_map, get_coordinates_of_a_key_in_a_yaml_object(
location if plain_error["type"] != "missing" else location[:-1], input_dictionary,
location if plain_error["type"] != "missing" else location[:-1],
)
if isinstance(input_dictionary, CommentedMap)
else None
), ),
) )
def parse_validation_errors( def parse_validation_errors(
exception: pydantic.ValidationError, exception: pydantic.ValidationError,
rendercv_dictionary_as_commented_map: CommentedMap, input_dictionary: CommentedMap | dict[str, Any],
) -> list[RenderCVValidationError]: ) -> list[RenderCVValidationError]:
"""Extract all validation errors from Pydantic exception with deduplication. """Extract all validation errors from Pydantic exception with deduplication.
@@ -117,9 +121,7 @@ def parse_validation_errors(
for plain_error in all_plain_errors: for plain_error in all_plain_errors:
all_final_errors.append( all_final_errors.append(
parse_plain_pydantic_error( parse_plain_pydantic_error(plain_error, input_dictionary)
plain_error, rendercv_dictionary_as_commented_map
)
) )
if plain_error["type"] == CustomPydanticErrorTypes.entry_validation.value: if plain_error["type"] == CustomPydanticErrorTypes.entry_validation.value:
@@ -129,9 +131,7 @@ def parse_validation_errors(
loc = plain_cause_error["loc"][1:] # Omit `entries` location loc = plain_cause_error["loc"][1:] # Omit `entries` location
plain_cause_error["loc"] = plain_error["loc"] + loc plain_cause_error["loc"] = plain_error["loc"] + loc
all_final_errors.append( all_final_errors.append(
parse_plain_pydantic_error( parse_plain_pydantic_error(plain_cause_error, input_dictionary)
plain_cause_error, rendercv_dictionary_as_commented_map
)
) )
# Remove duplicates from all_final_errors: # Remove duplicates from all_final_errors:
@@ -147,7 +147,8 @@ def parse_validation_errors(
def get_inner_yaml_object_from_its_key( def get_inner_yaml_object_from_its_key(
yaml_object: CommentedMap, location_key: str yaml_object: CommentedMap,
location_key: str,
) -> tuple[CommentedMap, tuple[tuple[int, int], tuple[int, int]]]: ) -> tuple[CommentedMap, tuple[tuple[int, int], tuple[int, int]]]:
"""Navigate one level into YAML structure and extract coordinates. """Navigate one level into YAML structure and extract coordinates.
@@ -189,7 +190,7 @@ def get_inner_yaml_object_from_its_key(
def get_coordinates_of_a_key_in_a_yaml_object( def get_coordinates_of_a_key_in_a_yaml_object(
yaml_object: ruamel.yaml.YAML, location: tuple[str, ...] yaml_object: CommentedMap, location: tuple[str, ...]
) -> tuple[tuple[int, int], tuple[int, int]]: ) -> tuple[tuple[int, int], tuple[int, int]]:
"""Resolve dotted location path to exact YAML source coordinates. """Resolve dotted location path to exact YAML source coordinates.
@@ -215,7 +216,7 @@ def get_coordinates_of_a_key_in_a_yaml_object(
((start_line, start_col), (end_line, end_col)) in 1-indexed coordinates. ((start_line, start_col), (end_line, end_col)) in 1-indexed coordinates.
""" """
current_yaml_object: ruamel.yaml.YAML = yaml_object current_yaml_object = yaml_object
coordinates = ((0, 0), (0, 0)) coordinates = ((0, 0), (0, 0))
# start from the first key and move forward: # start from the first key and move forward:
for location_key in location: for location_key in location:

View File

@@ -1,5 +1,5 @@
import pathlib import pathlib
from typing import TypedDict, Unpack from typing import Any, TypedDict, Unpack
import pydantic import pydantic
from ruamel.yaml.comments import CommentedMap from ruamel.yaml.comments import CommentedMap
@@ -101,7 +101,7 @@ def build_rendercv_dictionary(
def build_rendercv_model_from_commented_map( def build_rendercv_model_from_commented_map(
commented_map: CommentedMap, commented_map: CommentedMap | dict[str, Any],
input_file_path: pathlib.Path | None = None, input_file_path: pathlib.Path | None = None,
) -> RenderCVModel: ) -> RenderCVModel:
"""Validate merged dictionary and build Pydantic model with error mapping. """Validate merged dictionary and build Pydantic model with error mapping.

View File

@@ -170,7 +170,7 @@ def create_discriminator_field_spec(
Returns: Returns:
Tuple of Literal type annotation and Field with default value. Tuple of Literal type annotation and Field with default value.
""" """
field_annotation = Literal[discriminator_value] field_annotation = Literal[discriminator_value] # ty: ignore[invalid-type-form]
# Update description with new default value # Update description with new default value
updated_description = update_description_with_new_default( updated_description = update_description_with_new_default(

View File

@@ -187,7 +187,7 @@ class TestParseConnections:
connections = parse_connections(model) connections = parse_connections(model)
icons = [c.fontawesome_icon for c in connections] # type: ignore icons = [c.fontawesome_icon for c in connections]
assert icons == [ assert icons == [
fontawesome_icons["location"], fontawesome_icons["location"],
fontawesome_icons["email"], fontawesome_icons["email"],
@@ -208,7 +208,7 @@ class TestParseConnections:
connections = parse_connections(model) connections = parse_connections(model)
icons = [c.fontawesome_icon for c in connections] # type: ignore icons = [c.fontawesome_icon for c in connections]
assert icons == [ assert icons == [
fontawesome_icons["email"], fontawesome_icons["email"],
fontawesome_icons["LinkedIn"], fontawesome_icons["LinkedIn"],

View File

@@ -194,13 +194,8 @@ class TestRenderEntryTemplates:
current_date=Date(2024, 1, 1), current_date=Date(2024, 1, 1),
) )
assert ( assert entry.main_column == "**Solo**" # ty: ignore[unresolved-attribute]
entry.main_column == "**Solo**" # pyright: ignore [reportAttributeAccessIssue] assert entry.date_and_location_column == "" # ty: ignore[unresolved-attribute]
)
assert (
entry.date_and_location_column # pyright: ignore [reportAttributeAccessIssue]
== ""
)
def test_populates_highlights_and_date_placeholders(self): def test_populates_highlights_and_date_placeholders(self):
entry = NormalEntry( entry = NormalEntry(
@@ -218,8 +213,8 @@ class TestRenderEntryTemplates:
current_date=Date(2024, 1, 1), current_date=Date(2024, 1, 1),
) )
assert entry.main_column == "**Project**\n- Alpha\n- Beta" # pyright: ignore [reportAttributeAccessIssue] assert entry.main_column == "**Project**\n- Alpha\n- Beta" # ty: ignore[unresolved-attribute]
assert entry.date_and_location_column == "Remote\nMay 2023" # pyright: ignore [reportAttributeAccessIssue] assert entry.date_and_location_column == "Remote\nMay 2023" # ty: ignore[unresolved-attribute]
def test_formats_start_and_end_dates_in_custom_template(self): def test_formats_start_and_end_dates_in_custom_template(self):
entry = NormalEntry( entry = NormalEntry(
@@ -240,7 +235,7 @@ class TestRenderEntryTemplates:
current_date=Date(2024, 1, 1), current_date=Date(2024, 1, 1),
) )
assert entry.main_column == "Jan 2020 / Mar 2021 / / Jan 2020 Mar 2021" # pyright: ignore [reportAttributeAccessIssue] assert entry.main_column == "Jan 2020 / Mar 2021 / / Jan 2020 Mar 2021" # ty: ignore[unresolved-attribute]
def test_handles_authors_doi_and_date_placeholders(self): def test_handles_authors_doi_and_date_placeholders(self):
entry = PublicationEntry( entry = PublicationEntry(
@@ -263,7 +258,7 @@ class TestRenderEntryTemplates:
) )
assert ( assert (
entry.main_column # pyright: ignore [reportAttributeAccessIssue] entry.main_column # ty: ignore[unresolved-attribute]
== "Alice, Bob | [10.1000/xyz123](https://doi.org/10.1000/xyz123) | Feb" == "Alice, Bob | [10.1000/xyz123](https://doi.org/10.1000/xyz123) | Feb"
" 2024" " 2024"
) )
@@ -289,7 +284,7 @@ class TestRenderEntryTemplates:
) )
assert ( assert (
entry.main_column # pyright: ignore [reportAttributeAccessIssue] entry.main_column # ty: ignore[unresolved-attribute]
== "Linked Item [example.com/page](https://example.com/page/)" == "Linked Item [example.com/page](https://example.com/page/)"
) )

View File

@@ -68,7 +68,7 @@ class TestProcessFields:
fn, seen = recorder fn, seen = recorder
entry = EntryWithInt(name="Test", count=42) entry = EntryWithInt(name="Test", count=42)
process_fields(entry, [fn]) # pyright: ignore[reportArgumentType] process_fields(entry, [fn]) # ty: ignore[invalid-argument-type]
assert "Test" in seen assert "Test" in seen
assert "42" in seen assert "42" in seen
@@ -118,12 +118,12 @@ class TestProcessModel:
assert result.cv.headline == "Software Engineer @" assert result.cv.headline == "Software Engineer @"
# Connections and last updated date are added to cv # Connections and last updated date are added to cv
assert result.cv.connections == [ # pyright: ignore[reportAttributeAccessIssue] assert result.cv.connections == [ # ty: ignore[unresolved-attribute]
"[jane@example.com](mailto:jane@example.com)", "[jane@example.com](mailto:jane@example.com)",
"[janedoe.dev](https://janedoe.dev/)", "[janedoe.dev](https://janedoe.dev/)",
] ]
assert ( assert (
result.cv.top_note == "*Last updated in Feb 2024*" # pyright: ignore[reportAttributeAccessIssue] result.cv.top_note == "*Last updated in Feb 2024*" # ty: ignore[unresolved-attribute]
) )
entry = result.cv.rendercv_sections[0].entries[0] entry = result.cv.rendercv_sections[0].entries[0]
@@ -157,13 +157,13 @@ class TestProcessModel:
entry.date_and_location_column == "#strong[Remote]\nJan 2022 Feb 2023" entry.date_and_location_column == "#strong[Remote]\nJan 2022 Feb 2023"
) )
# Connections rendered as Typst links with icons by default # Connections rendered as Typst links with icons by default
assert result.cv.connections[0].startswith("#link(") # pyright: ignore[reportAttributeAccessIssue] assert result.cv.connections[0].startswith("#link(") # ty: ignore[unresolved-attribute]
assert "#connection-with-icon" in result.cv.connections[0] # pyright: ignore[reportAttributeAccessIssue] assert "#connection-with-icon" in result.cv.connections[0] # ty: ignore[unresolved-attribute]
else: else:
assert "- Improved Python performance" in entry.main_column assert "- Improved Python performance" in entry.main_column
assert entry.date_and_location_column == "Remote\nJan 2022 Feb 2023" assert entry.date_and_location_column == "Remote\nJan 2022 Feb 2023"
assert result.cv.connections[0].startswith("#link(") # pyright: ignore[reportAttributeAccessIssue] assert result.cv.connections[0].startswith("#link(") # ty: ignore[unresolved-attribute]
assert "jane@example.com" in result.cv.connections[0] # pyright: ignore[reportAttributeAccessIssue] assert "jane@example.com" in result.cv.connections[0] # ty: ignore[unresolved-attribute]
def test_handles_cv_with_no_sections(self): def test_handles_cv_with_no_sections(self):
cv_data = { cv_data = {

View File

@@ -1,4 +1,5 @@
import copy import copy
from typing import Any
import pytest import pytest
@@ -70,49 +71,49 @@ reversed_numbered_entry_dictionary = {
@pytest.fixture @pytest.fixture
def publication_entry() -> dict[str, str | list[str]]: def publication_entry() -> dict[str, Any]:
"""Return a sample publication entry.""" """Return a sample publication entry."""
return copy.deepcopy(publication_entry_dictionary) return copy.deepcopy(publication_entry_dictionary)
@pytest.fixture @pytest.fixture
def experience_entry() -> dict[str, str]: def experience_entry() -> dict[str, Any]:
"""Return a sample experience entry.""" """Return a sample experience entry."""
return copy.deepcopy(experience_entry_dictionary) return copy.deepcopy(experience_entry_dictionary)
@pytest.fixture @pytest.fixture
def education_entry() -> dict[str, str]: def education_entry() -> dict[str, Any]:
"""Return a sample education entry.""" """Return a sample education entry."""
return copy.deepcopy(education_entry_dictionary) return copy.deepcopy(education_entry_dictionary)
@pytest.fixture @pytest.fixture
def normal_entry() -> dict[str, str]: def normal_entry() -> dict[str, Any]:
"""Return a sample normal entry.""" """Return a sample normal entry."""
return copy.deepcopy(normal_entry_dictionary) return copy.deepcopy(normal_entry_dictionary)
@pytest.fixture @pytest.fixture
def one_line_entry() -> dict[str, str]: def one_line_entry() -> dict[str, Any]:
"""Return a sample one line entry.""" """Return a sample one line entry."""
return copy.deepcopy(one_line_entry_dictionary) return copy.deepcopy(one_line_entry_dictionary)
@pytest.fixture @pytest.fixture
def bullet_entry() -> dict[str, str]: def bullet_entry() -> dict[str, Any]:
"""Return a sample bullet entry.""" """Return a sample bullet entry."""
return copy.deepcopy(bullet_entry_dictionary) return copy.deepcopy(bullet_entry_dictionary)
@pytest.fixture @pytest.fixture
def numbered_entry() -> dict[str, str]: def numbered_entry() -> dict[str, Any]:
"""Return a sample numbered entry.""" """Return a sample numbered entry."""
return copy.deepcopy(numbered_entry_dictionary) return copy.deepcopy(numbered_entry_dictionary)
@pytest.fixture @pytest.fixture
def reversed_numbered_entry() -> dict[str, str]: def reversed_numbered_entry() -> dict[str, Any]:
"""Return a sample reversed numbered entry.""" """Return a sample reversed numbered entry."""
return copy.deepcopy(reversed_numbered_entry_dictionary) return copy.deepcopy(reversed_numbered_entry_dictionary)

View File

@@ -1,3 +1,5 @@
from typing import Any
import pydantic import pydantic
import pytest import pytest
@@ -25,7 +27,7 @@ class TestCv:
"sections": sections, "sections": sections,
} }
cv = Cv(**input) cv = Cv.model_validate(input)
assert len(cv.rendercv_sections) == len(available_entry_type_names) assert len(cv.rendercv_sections) == len(available_entry_type_names)
for section in cv.rendercv_sections: for section in cv.rendercv_sections:
@@ -45,10 +47,10 @@ class TestCv:
} }
with pytest.raises(pydantic.ValidationError): with pytest.raises(pydantic.ValidationError):
Cv(**input) Cv.model_validate(input)
def test_rejects_invalid_entries(self): def test_rejects_invalid_entries(self):
input = {"name": "John Doe", "sections": {}} input: dict[str, Any] = {"name": "John Doe", "sections": {}}
input["sections"]["section_title"] = [ input["sections"]["section_title"] = [
{ {
"this": "is", "this": "is",
@@ -58,16 +60,16 @@ class TestCv:
] ]
with pytest.raises(pydantic.ValidationError): with pytest.raises(pydantic.ValidationError):
Cv(**input) Cv.model_validate(input)
def test_rejects_section_without_list(self): def test_rejects_section_without_list(self):
input = {"name": "John Doe", "sections": {}} input: dict[str, Any] = {"name": "John Doe", "sections": {}}
input["sections"]["section_title"] = { input["sections"]["section_title"] = {
"this section": "does not have a list of entries but a single entry." "this section": "does not have a list of entries but a single entry."
} }
with pytest.raises(pydantic.ValidationError): with pytest.raises(pydantic.ValidationError):
Cv(**input) Cv.model_validate(input)
def test_phone_serialization(self): def test_phone_serialization(self):
input_data = {"name": "John Doe", "phone": "+905419999999"} input_data = {"name": "John Doe", "phone": "+905419999999"}

View File

@@ -86,7 +86,7 @@ class TestSocialNetwork:
( (
"Bluesky", "Bluesky",
"myusername.bsky.social", "myusername.bsky.social",
"https://bsky.app/profile/myusername.bsky.social" "https://bsky.app/profile/myusername.bsky.social",
), ),
], ],
) )

View File

@@ -1,3 +1,5 @@
from typing import Any
import pytest import pytest
from rendercv.exception import RenderCVUserError from rendercv.exception import RenderCVUserError
@@ -136,7 +138,7 @@ class TestUpdateValueByLocation:
assert original == {"name": "Jane"} assert original == {"name": "Jane"}
def test_deeply_nested_structure(self): def test_deeply_nested_structure(self):
initial = { initial: dict[str, Any] = {
"cv": { "cv": {
"sections": { "sections": {
"education": [ "education": [
@@ -230,7 +232,7 @@ class TestApplyOverridesToDictionary:
assert result is not original assert result is not original
def test_complex_cv_scenario(self): def test_complex_cv_scenario(self):
initial = { initial: dict[str, Any] = {
"cv": { "cv": {
"name": "John Doe", "name": "John Doe",
"sections": { "sections": {

View File

@@ -289,7 +289,7 @@ class TestCreateNestedFieldSpec:
# Check that a variant class was created with default_factory # Check that a variant class was created with default_factory
assert field.default_factory is not None assert field.default_factory is not None
assert issubclass(field.default_factory, pydantic.BaseModel) # pyright: ignore[reportArgumentType] assert issubclass(field.default_factory, pydantic.BaseModel) # ty: ignore[invalid-argument-type]
# Instantiate to check default values # Instantiate to check default values
instance = field.default_factory() instance = field.default_factory()
@@ -311,7 +311,7 @@ class TestCreateNestedFieldSpec:
# Check that a variant class was created with default_factory # Check that a variant class was created with default_factory
assert field.default_factory is not None assert field.default_factory is not None
assert issubclass(field.default_factory, pydantic.BaseModel) # pyright: ignore[reportArgumentType] assert issubclass(field.default_factory, pydantic.BaseModel) # ty: ignore[invalid-argument-type]
# Instantiate to check default values # Instantiate to check default values
instance = field.default_factory() instance = field.default_factory()
@@ -352,7 +352,7 @@ class TestCreateNestedFieldSpec:
# Check that a variant class was created with default_factory # Check that a variant class was created with default_factory
assert field.default_factory is not None assert field.default_factory is not None
assert issubclass(field.default_factory, pydantic.BaseModel) # pyright: ignore[reportArgumentType] assert issubclass(field.default_factory, pydantic.BaseModel) # ty: ignore[invalid-argument-type]
# Instantiate to check default values # Instantiate to check default values
instance = field.default_factory() instance = field.default_factory()
@@ -754,7 +754,7 @@ class TestCreateVariantPydanticModel:
pydantic.Field(default=old_default, description=base_description), pydantic.Field(default=old_default, description=base_description),
), ),
} }
Base = pydantic.create_model("Base", **base_fields) Base = pydantic.create_model("Base", **base_fields) # ty: ignore[no-matching-overload]
VariantClass = create_variant_pydantic_model( VariantClass = create_variant_pydantic_model(
variant_name="custom", variant_name="custom",
@@ -1045,8 +1045,8 @@ class TestCreateNestedModelVariantModel:
# The variant should be created without errors # The variant should be created without errors
instance = variant_class() instance = variant_class()
assert instance.x == 100 # pyright: ignore[reportAttributeAccessIssue] assert instance.x == 100 # ty: ignore[unresolved-attribute]
assert instance.y == 2 # pyright: ignore[reportAttributeAccessIssue] assert instance.y == 2 # ty: ignore[unresolved-attribute]
# nonexistent_field should not be in the instance # nonexistent_field should not be in the instance
assert not hasattr(instance, "nonexistent_field") assert not hasattr(instance, "nonexistent_field")
@@ -1065,5 +1065,5 @@ class TestCreateNestedModelVariantModel:
variant_class = create_nested_model_variant_model(ModelWithPlainDict, updates) variant_class = create_nested_model_variant_model(ModelWithPlainDict, updates)
instance = variant_class() instance = variant_class()
assert instance.metadata == {"new_key": "new_value"} # pyright: ignore[reportAttributeAccessIssue] assert instance.metadata == {"new_key": "new_value"} # ty: ignore[unresolved-attribute]
assert instance.count == 10 # pyright: ignore[reportAttributeAccessIssue] assert instance.count == 10 # ty: ignore[unresolved-attribute]

84
uv.lock generated
View File

@@ -979,19 +979,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/96/fd59c1532891762ea4815e73956c532053d5e26d56969e1e5d1e4ca4b207/pymupdf-1.26.5-cp39-abi3-win_amd64.whl", hash = "sha256:39a6fb58182b27b51ea8150a0cd2e4ee7e0cf71e9d6723978f28699b42ee61ae", size = 18747258, upload-time = "2025-10-10T14:01:37.346Z" }, { url = "https://files.pythonhosted.org/packages/c6/96/fd59c1532891762ea4815e73956c532053d5e26d56969e1e5d1e4ca4b207/pymupdf-1.26.5-cp39-abi3-win_amd64.whl", hash = "sha256:39a6fb58182b27b51ea8150a0cd2e4ee7e0cf71e9d6723978f28699b42ee61ae", size = 18747258, upload-time = "2025-10-10T14:01:37.346Z" },
] ]
[[package]]
name = "pyright"
version = "1.1.407"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "9.0.2" version = "9.0.2"
@@ -1152,11 +1139,11 @@ create-executable = [
dev = [ dev = [
{ name = "black" }, { name = "black" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "pyright" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-xdist" }, { name = "pytest-xdist" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
] ]
docs = [ docs = [
{ name = "markdown-callouts" }, { name = "markdown-callouts" },
@@ -1193,11 +1180,11 @@ create-executable = [{ name = "pyinstaller", specifier = ">=6.17.0" }]
dev = [ dev = [
{ name = "black", specifier = ">=25.12.0" }, { name = "black", specifier = ">=25.12.0" },
{ name = "pre-commit", specifier = ">=4.5.0" }, { name = "pre-commit", specifier = ">=4.5.0" },
{ name = "pyright", specifier = ">=1.1.407" },
{ name = "pytest", specifier = ">=9.0.2" }, { name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "ruff", specifier = ">=0.14.8" }, { name = "ruff", specifier = ">=0.14.10" },
{ name = "ty", specifier = ">=0.0.5" },
] ]
docs = [ docs = [
{ name = "markdown-callouts", specifier = ">=0.4.0" }, { name = "markdown-callouts", specifier = ">=0.4.0" },
@@ -1302,28 +1289,28 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.8" version = "0.14.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
] ]
[[package]] [[package]]
@@ -1374,6 +1361,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" }, { url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" },
] ]
[[package]]
name = "ty"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" },
{ url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" },
{ url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" },
{ url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" },
{ url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" },
{ url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" },
{ url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" },
{ url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" },
{ url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" },
{ url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" },
{ url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" },
{ url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" },
{ url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" },
{ url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.20.0" version = "0.20.0"