Coverage for src/somesy/rust/models.py: 85%
61 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +0000
1"""Pyproject models."""
3import re
4from pathlib import Path
5from typing import Annotated
7from packaging.version import parse as parse_version
8from pydantic import BaseModel, Field, field_validator, model_validator
10from somesy.core.types import HttpUrlStr
13class RustConfig(BaseModel):
14 """Rust configuration model."""
16 model_config = {"use_enum_values": True}
18 name: Annotated[
19 str,
20 Field(
21 pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$",
22 max_length=64,
23 description="Package name",
24 ),
25 ]
26 version: Annotated[
27 str,
28 Field(
29 pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$",
30 description="Package version",
31 ),
32 ]
33 description: Annotated[str | None, Field(description="Package description")] = None
34 license: Annotated[
35 str | None,
36 Field(
37 description="A combination SPDX license identifiers with AND, OR and so on."
38 ),
39 ] = None
40 authors: Annotated[set[str], Field(description="Package authors")]
41 maintainers: Annotated[
42 set[str] | None, Field(description="Package maintainers")
43 ] = None
44 readme: Annotated[
45 Path | list[Path] | None, Field(description="Package readme file(s)")
46 ] = None
47 license_file: Annotated[Path | None, Field(description="Package license file")] = (
48 None
49 )
50 homepage: Annotated[HttpUrlStr | None, Field(description="Package homepage")] = None
51 repository: Annotated[
52 HttpUrlStr | None, Field(description="Package repository")
53 ] = None
54 documentation: Annotated[
55 HttpUrlStr | None, Field(description="Package documentation page")
56 ] = None
57 keywords: Annotated[
58 set[str] | None, Field(description="Keywords that describe the package")
59 ] = None
60 classifiers: Annotated[list[str] | None, Field(description="pypi classifiers")] = (
61 None
62 )
63 urls: Annotated[dict[str, HttpUrlStr] | None, Field(description="Package URLs")] = (
64 None
65 )
67 @model_validator(mode="before")
68 @classmethod
69 def license_or_file(cls, values):
70 """License and license file are mutually exclusive."""
71 if "license" in values and "license_file" in values:
72 raise ValueError("license and license_file are mutually exclusive")
73 return values
75 @field_validator("version")
76 @classmethod
77 def validate_version(cls, v):
78 """Validate version using PEP 440."""
79 try:
80 _ = parse_version(v)
81 except ValueError as err:
82 raise ValueError("Invalid version") from err
83 return v
85 @field_validator("readme", "license_file")
86 @classmethod
87 def validate_readme(cls, v):
88 """Validate readme file(s) by checking whether files exist."""
89 if isinstance(v, list):
90 if any(not e.is_file() for e in v):
91 raise ValueError("Some file(s) do not exist")
92 else:
93 if not v.is_file():
94 raise ValueError("File does not exist")
96 @field_validator("keywords")
97 @classmethod
98 def check_keywords_field(cls, v):
99 """Check the keywords field."""
100 if v is None:
101 return v
103 # Check if number of keywords is at most 5
104 if len(v) > 5:
105 raise ValueError("A maximum of 5 keywords is allowed")
107 for keyword in v or []:
108 check_keyword(keyword)
110 return v
113def check_keyword(keyword: str):
114 """Check if keyword is valid."""
115 # Check if keyword is ASCII and has at most 20 characters
116 if not keyword.isascii() or len(keyword) > 20:
117 raise ValueError(
118 "Each keyword must be ASCII text and have at most 20 characters"
119 )
121 # Check if keyword starts with an alphanumeric character
122 if not re.match(r"^[a-zA-Z0-9]", keyword):
123 raise ValueError("Each keyword must start with an alphanumeric character")
125 # Check if keyword contains only allowed characters
126 if not re.match(r"^[a-zA-Z0-9_\-+]+$", keyword):
127 raise ValueError("Keywords can only contain letters, numbers, _, -, or +")