Coverage for src/somesy/package_json/models.py: 98%
60 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-10 14:33 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-10 14:33 +0000
1"""package.json validation models."""
2import re
3from typing import List, Optional, Union
5from pydantic import AnyUrl, BaseModel, EmailStr, Field, ValidationError, validator
6from typing_extensions import Annotated
9class PackageAuthor(BaseModel):
10 """Package author model."""
12 name: Annotated[Optional[str], Field(description="Author name")]
13 email: Annotated[Optional[EmailStr], Field(description="Author email")]
14 url: Annotated[Optional[AnyUrl], Field(description="Author website or orcid page")]
17class PackageRepository(BaseModel):
18 """Package repository model."""
20 type: Annotated[Optional[str], Field(description="Repository type")]
21 url: Annotated[str, Field(description="Repository url")]
24class PackageLicense(BaseModel):
25 """Package license model."""
27 type: Annotated[Optional[str], Field(description="License type")]
28 url: Annotated[str, Field(description="License url")]
31class PackageJsonConfig(BaseModel):
32 """Package.json config model."""
34 name: Annotated[str, Field(description="Package name")]
35 version: Annotated[str, Field(description="Package version")]
36 description: Annotated[Optional[str], Field(description="Package description")]
37 author: Annotated[
38 Optional[Union[str, PackageAuthor]], Field(description="Package author")
39 ]
40 maintainers: Annotated[
41 Optional[List[Union[str, PackageAuthor]]],
42 Field(description="Package maintainers"),
43 ]
44 contributors: Annotated[
45 Optional[List[Union[str, PackageAuthor]]],
46 Field(description="Package contributors"),
47 ]
48 license: Annotated[
49 Optional[Union[str, PackageLicense]], Field(description="Package license")
50 ]
51 repository: Annotated[
52 Optional[Union[PackageRepository, str]], Field(description="Package repository")
53 ]
54 homepage: Annotated[Optional[AnyUrl], Field(description="Package homepage")]
55 keywords: Annotated[
56 Optional[List[str]], Field(description="Keywords that describe the package")
57 ]
59 # convert package author to dict if it is a string
60 @classmethod
61 def convert_author(cls, author: str) -> PackageAuthor:
62 """Convert author string to PackageAuthor model."""
63 # parse author string to "name <email> (url)" format with regex
64 author_regex = r"^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$"
65 author_match = re.match(author_regex, author)
66 if not author_match:
67 raise ValidationError(f"Invalid author format: {author}")
68 author_name = author_match[1]
69 author_email = author_match[2]
70 author_url = author_match[3]
72 return PackageAuthor(name=author_name, email=author_email, url=author_url)
74 @validator("name")
75 def validate_name(cls, v):
76 """Validate package name."""
77 pattern = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$"
78 if re.match(pattern, v) is None:
79 raise ValidationError("Invalid name")
81 return v
83 @validator("version")
84 def validate_version(cls, v):
85 """Validate package version."""
86 # pattern for npm version
87 pattern = r"^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
88 if re.match(pattern, v) is None:
89 raise ValidationError("Invalid version")
90 return v
92 @validator("author")
93 def validate_author(cls, v):
94 """Validate package author."""
95 return cls.convert_author(v) if isinstance(v, str) else v
97 @validator("maintainers", "contributors")
98 def validate_people(cls, v):
99 """Validate package maintainers and contributors."""
100 people = []
101 for p in v:
102 if isinstance(p, str):
103 people.append(cls.convert_author(p))
104 else:
105 people.append(p)
106 return people
108 class Config:
109 """Pydantic config."""
111 allow_population_by_field_name = True