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:42 +0000

1"""package.json validation models.""" 

2 

3import re 

4from logging import getLogger 

5from typing import List, Optional, Union 

6 

7from pydantic import BaseModel, EmailStr, Field, field_validator 

8from typing_extensions import Annotated 

9 

10from somesy.core.types import HttpUrlStr 

11 

12logger = getLogger("somesy") 

13 

14 

15class PackageAuthor(BaseModel): 

16 """Package author model.""" 

17 

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 

23 

24 

25class PackageRepository(BaseModel): 

26 """Package repository model.""" 

27 

28 type: Annotated[Optional[str], Field(description="Repository type")] = None 

29 url: Annotated[str, Field(description="Repository url")] 

30 

31 

32class PackageLicense(BaseModel): 

33 """Package license model.""" 

34 

35 type: Annotated[Optional[str], Field(description="License type")] = None 

36 url: Annotated[str, Field(description="License url")] 

37 

38 

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 

42 

43 

44class PackageJsonConfig(BaseModel): 

45 """Package.json config model.""" 

46 

47 model_config = dict(populate_by_name=True) 

48 

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 

77 

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] 

89 

90 if author_email is None: 

91 return None 

92 return PackageAuthor(name=author_name, email=author_email, url=author_url) 

93 

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") 

100 

101 return v 

102 

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 

110 

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 

116 

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