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

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

2import re 

3from typing import List, Optional, Union 

4 

5from pydantic import AnyUrl, BaseModel, EmailStr, Field, ValidationError, validator 

6from typing_extensions import Annotated 

7 

8 

9class PackageAuthor(BaseModel): 

10 """Package author model.""" 

11 

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

15 

16 

17class PackageRepository(BaseModel): 

18 """Package repository model.""" 

19 

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

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

22 

23 

24class PackageLicense(BaseModel): 

25 """Package license model.""" 

26 

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

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

29 

30 

31class PackageJsonConfig(BaseModel): 

32 """Package.json config model.""" 

33 

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 ] 

58 

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] 

71 

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

73 

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

80 

81 return v 

82 

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 

91 

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 

96 

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 

107 

108 class Config: 

109 """Pydantic config.""" 

110 

111 allow_population_by_field_name = True