Coverage for src/somesy/package_json/models.py: 82%
73 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-29 07:50 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-29 07:50 +0000
1"""package.json validation models."""
3import re
4from logging import getLogger
5from typing import List, Optional, Union
7from pydantic import BaseModel, EmailStr, Field, field_validator
8from typing_extensions import Annotated
10from somesy.core.types import HttpUrlStr
12logger = getLogger("somesy")
15class PackageAuthor(BaseModel):
16 """Package author model."""
18 name: Annotated[Optional[str], Field(description="Author name")]
19 email: Annotated[Optional[EmailStr], Field(description="Author email")] = None
20 url: Annotated[
21 Optional[HttpUrlStr], Field(description="Author website or orcid page")
22 ] = None
25class PackageRepository(BaseModel):
26 """Package repository model."""
28 type: Annotated[Optional[str], Field(description="Repository type")] = None
29 url: Annotated[str, Field(description="Repository url")]
32class PackageLicense(BaseModel):
33 """Package license model."""
35 type: Annotated[Optional[str], Field(description="License type")] = None
36 url: Annotated[str, Field(description="License url")]
39NPM_PKG_AUTHOR = r"^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$"
40NPM_PKG_NAME = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$"
41NPM_PKG_VERSION = 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-]+)*))?$" # noqa: E501
44class PackageJsonConfig(BaseModel):
45 """Package.json config model."""
47 model_config = dict(populate_by_name=True)
49 name: Annotated[str, Field(description="Package name")]
50 version: Annotated[str, Field(description="Package version")]
51 description: Annotated[Optional[str], Field(description="Package description")] = (
52 None
53 )
54 author: Annotated[
55 Optional[Union[str, PackageAuthor]], Field(description="Package author")
56 ] = None
57 maintainers: Annotated[
58 Optional[List[Union[str, PackageAuthor]]],
59 Field(description="Package maintainers"),
60 ] = None
61 contributors: Annotated[
62 Optional[List[Union[str, PackageAuthor]]],
63 Field(description="Package contributors"),
64 ] = None
65 license: Annotated[
66 Optional[Union[str, PackageLicense]], Field(description="Package license")
67 ] = None
68 repository: Annotated[
69 Optional[Union[PackageRepository, str]], Field(description="Package repository")
70 ] = None
71 homepage: Annotated[Optional[HttpUrlStr], Field(description="Package homepage")] = (
72 None
73 )
74 keywords: Annotated[
75 Optional[List[str]], Field(description="Keywords that describe the package")
76 ] = None
78 # convert package author to dict if it is a string
79 @classmethod
80 def convert_author(cls, author: str) -> Optional[PackageAuthor]:
81 """Convert author string to PackageAuthor model."""
82 # parse author string to "name <email> (url)" format with regex
83 author_match = re.match(NPM_PKG_AUTHOR, author)
84 if not author_match:
85 raise ValueError(f"Invalid author format: {author}")
86 author_name = author_match[1]
87 author_email = author_match[2]
88 author_url = author_match[3]
90 if author_email is None:
91 return None
92 return PackageAuthor(name=author_name, email=author_email, url=author_url)
94 @field_validator("name")
95 @classmethod
96 def validate_name(cls, v):
97 """Validate package name."""
98 if re.match(NPM_PKG_NAME, v) is None:
99 raise ValueError("Invalid name")
101 return v
103 @field_validator("version")
104 @classmethod
105 def validate_version(cls, v):
106 """Validate package version."""
107 if re.match(NPM_PKG_VERSION, v) is None:
108 raise ValueError("Invalid version")
109 return v
111 @field_validator("author")
112 @classmethod
113 def validate_author(cls, v):
114 """Validate package author."""
115 return cls.convert_author(v) if isinstance(v, str) else v
117 @field_validator("maintainers", "contributors")
118 @classmethod
119 def validate_people(cls, v):
120 """Validate package maintainers and contributors."""
121 people = []
122 for p in v:
123 if isinstance(p, str):
124 author = cls.convert_author(p)
125 if author is not None:
126 people.append(cls.convert_author(p))
127 else:
128 logger.warning(
129 f"Invalid email format for maintainer/contributor {p}, omitting."
130 )
131 elif p.email is not None:
132 people.append(p)
133 else:
134 logger.warning(
135 f"Invalid email format for maintainer/contributor {p}, omitting."
136 )
137 return people