Coverage for src/somesy/package_json/models.py: 88%

64 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-30 09:42 +0000

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

2import re 

3from typing import List, Optional, Union 

4 

5from pydantic import BaseModel, EmailStr, Field, field_validator 

6from typing_extensions import Annotated 

7 

8from somesy.core.types import HttpUrlStr 

9 

10 

11class PackageAuthor(BaseModel): 

12 """Package author model.""" 

13 

14 name: Annotated[Optional[str], Field(description="Author name")] 

15 email: Annotated[Optional[EmailStr], Field(description="Author email")] = None 

16 url: Annotated[ 

17 Optional[HttpUrlStr], Field(description="Author website or orcid page") 

18 ] = None 

19 

20 

21class PackageRepository(BaseModel): 

22 """Package repository model.""" 

23 

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

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

26 

27 

28class PackageLicense(BaseModel): 

29 """Package license model.""" 

30 

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

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

33 

34 

35NPM_PKG_AUTHOR = r"^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$" 

36NPM_PKG_NAME = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$" 

37NPM_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 

38 

39 

40class PackageJsonConfig(BaseModel): 

41 """Package.json config model.""" 

42 

43 model_config = dict(populate_by_name=True) 

44 

45 name: Annotated[str, Field(description="Package name")] 

46 version: Annotated[str, Field(description="Package version")] 

47 description: Annotated[ 

48 Optional[str], Field(description="Package description") 

49 ] = None 

50 author: Annotated[ 

51 Optional[Union[str, PackageAuthor]], Field(description="Package author") 

52 ] = None 

53 maintainers: Annotated[ 

54 Optional[List[Union[str, PackageAuthor]]], 

55 Field(description="Package maintainers"), 

56 ] = None 

57 contributors: Annotated[ 

58 Optional[List[Union[str, PackageAuthor]]], 

59 Field(description="Package contributors"), 

60 ] = None 

61 license: Annotated[ 

62 Optional[Union[str, PackageLicense]], Field(description="Package license") 

63 ] = None 

64 repository: Annotated[ 

65 Optional[Union[PackageRepository, str]], Field(description="Package repository") 

66 ] = None 

67 homepage: Annotated[ 

68 Optional[HttpUrlStr], Field(description="Package homepage") 

69 ] = None 

70 keywords: Annotated[ 

71 Optional[List[str]], Field(description="Keywords that describe the package") 

72 ] = None 

73 

74 # convert package author to dict if it is a string 

75 @classmethod 

76 def convert_author(cls, author: str) -> PackageAuthor: 

77 """Convert author string to PackageAuthor model.""" 

78 # parse author string to "name <email> (url)" format with regex 

79 author_match = re.match(NPM_PKG_AUTHOR, author) 

80 if not author_match: 

81 raise ValueError(f"Invalid author format: {author}") 

82 author_name = author_match[1] 

83 author_email = author_match[2] 

84 author_url = author_match[3] 

85 

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

87 

88 @field_validator("name") 

89 @classmethod 

90 def validate_name(cls, v): 

91 """Validate package name.""" 

92 if re.match(NPM_PKG_NAME, v) is None: 

93 raise ValueError("Invalid name") 

94 

95 return v 

96 

97 @field_validator("version") 

98 @classmethod 

99 def validate_version(cls, v): 

100 """Validate package version.""" 

101 if re.match(NPM_PKG_VERSION, v) is None: 

102 raise ValueError("Invalid version") 

103 return v 

104 

105 @field_validator("author") 

106 @classmethod 

107 def validate_author(cls, v): 

108 """Validate package author.""" 

109 return cls.convert_author(v) if isinstance(v, str) else v 

110 

111 @field_validator("maintainers", "contributors") 

112 @classmethod 

113 def validate_people(cls, v): 

114 """Validate package maintainers and contributors.""" 

115 people = [] 

116 for p in v: 

117 if isinstance(p, str): 

118 people.append(cls.convert_author(p)) 

119 else: 

120 people.append(p) 

121 return people