1+ # Generate a simple script based on provided list for package types
2+
3+ """
4+ {
5+ "$schema": "https://packageurl.org/schemas/purl-type-definition.schema-1.0.json",
6+ "$id": "https://packageurl.org/types/pypi-definition.json",
7+ "type": "pypi",
8+ "type_name": "PyPI",
9+ "description": "Python packages",
10+ "repository": {
11+ "use_repository": true,
12+ "default_repository_url": "https://pypi.org",
13+ "note": "Previously https://pypi.python.org"
14+ },
15+ "namespace_definition": {
16+ "requirement": "prohibited",
17+ "note": "there is no namespace"
18+ },
19+ "name_definition": {
20+ "native_name": "name",
21+ "case_sensitive": false,
22+ "normalization_rules": [
23+ "Replace underscore _ with dash -",
24+ "Replace dot . with underscore _ when used in distribution (sdist, wheel) names"
25+ ],
26+ "note": "PyPI treats - and _ as the same character and is not case sensitive. Therefore a PyPI package name must be lowercased and underscore _ replaced with a dash -. Note that PyPI itself is preserving the case of package names. When used in distribution and wheel names, the dot . is replaced with an underscore _"
27+ },
28+ "version_definition": {
29+ "case_sensitive": false,
30+ "native_name": "version"
31+ },
32+ "qualifiers_definition": [
33+ {
34+ "key": "file_name",
35+ "requirement": "optional",
36+ "description": "The file_name qualifier selects a particular distribution file (case-sensitive). For naming convention, see the Python Packaging User Guide on source distributions https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-name and on binary distributions https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention and the rules for platform compatibility tags https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/"
37+ }
38+ ],
39+ "examples": [
40+ "pkg:pypi/django@1.11.1",
41+ "pkg:pypi/django@1.11.1?filename=Django-1.11.1.tar.gz",
42+ "pkg:pypi/django@1.11.1?filename=Django-1.11.1-py2.py3-none-any.whl",
43+ "pkg:pypi/django-allauth@12.23"
44+ ]
45+ }
46+ """
47+ from packageurl import PackageURL
48+ from pathlib import Path
49+ import json
50+
51+ HEADER = '''# Copyright (c) the purl authors
52+ # SPDX-License-Identifier: MIT
53+ #
54+ # Permission is hereby granted, free of charge, to any person obtaining a copy
55+ # of this software and associated documentation files (the "Software"), to deal
56+ # in the Software without restriction, including without limitation the rights
57+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
58+ # copies of the Software, and to permit persons to whom the Software is
59+ # furnished to do so, subject to the following conditions:
60+ #
61+ # The above copyright notice and this permission notice shall be included in all
62+ # copies or substantial portions of the Software.
63+ #
64+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
65+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
66+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
67+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
68+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
69+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
70+ # SOFTWARE.
71+
72+ # Visit https://github.com/package-url/packageurl-python for support and
73+ # download.
74+
75+ """
76+ Validate each type according to the PURL spec type definitions
77+ """
78+
79+ class TypeValidator:
80+ @classmethod
81+ def validate(cls, purl, strict=False):
82+ if not strict:
83+ purl = cls.normalize(purl)
84+
85+ if cls.namespace_requirement == "prohibited" and purl.namespace:
86+ yield f"Namespace is prohibited for purl type: {cls.type!r}"
87+
88+ elif cls.namespace_requirement == "required" and not purl.namespace:
89+ yield f"Namespace is required for purl type: {cls.type!r}"
90+
91+ if (
92+ not cls.namespace_case_sensitive
93+ and purl.namespace
94+ and purl.namespace.lower() != purl.namespace
95+ ):
96+ yield f"Namespace is not lowercased for purl type: {cls.type!r}"
97+
98+ if not cls.name_case_sensitive and purl.name and purl.name.lower() != purl.name:
99+ yield f"Name is not lowercased for purl type: {cls.type!r}"
100+
101+ if not cls.version_case_sensitive and purl.version and purl.version.lower() != purl.version:
102+ yield f"Version is not lowercased for purl type: {cls.type!r}"
103+
104+ yield from cls.validate_type(purl, strict=strict)
105+
106+ @classmethod
107+ def normalize(cls, purl):
108+ from packageurl import PackageURL
109+ from packageurl import normalize
110+
111+ type_norm, namespace_norm, name_norm, version_norm, qualifiers_norm, subpath_norm = (
112+ normalize(
113+ purl.type,
114+ purl.namespace,
115+ purl.name,
116+ purl.version,
117+ purl.qualifiers,
118+ purl.subpath,
119+ encode=False,
120+ )
121+ )
122+
123+ return PackageURL(
124+ type=type_norm,
125+ namespace=namespace_norm,
126+ name=name_norm,
127+ version=version_norm,
128+ qualifiers=qualifiers_norm,
129+ subpath=subpath_norm,
130+ )
131+
132+ @classmethod
133+ def validate_type(cls, purl, strict=False):
134+ if strict:
135+ yield from cls.validate_qualifiers(purl=purl)
136+
137+ @classmethod
138+ def validate_qualifiers(cls, purl):
139+ if not purl.qualifiers:
140+ return
141+
142+ purl_qualifiers_keys = set(purl.qualifiers.keys())
143+ allowed_qualifiers_set = cls.allowed_qualifiers
144+
145+ disallowed = purl_qualifiers_keys - allowed_qualifiers_set
146+
147+ if disallowed:
148+ yield (
149+ f"Invalid qualifiers found: {', '.join(sorted(disallowed))}. "
150+ f"Allowed qualifiers are: {', '.join(sorted(allowed_qualifiers_set))}"
151+ )
152+ '''
153+
154+
155+ TEMPLATE = """
156+ class {class_name}({validator_class}):
157+ type = "{type}"
158+ type_name = "{type_name}"
159+ description = '''{description}'''
160+ use_repository = {use_repository}
161+ default_repository_url = "{default_repository_url}"
162+ namespace_requirement = "{namespace_requirement}"
163+ allowed_qualifiers = {allowed_qualifiers}
164+ namespace_case_sensitive = {namespace_case_sensitive}
165+ name_case_sensitive = {name_case_sensitive}
166+ version_case_sensitive = {version_case_sensitive}
167+ purl_pattern = "{purl_pattern}"
168+ """
169+
170+
171+ def generate_validators ():
172+ """
173+ Generate validators for all package types defined in the packageurl specification.
174+ """
175+
176+ base_dir = Path (__file__ ).parent .parent .parent
177+
178+ types_dir = base_dir / "spec" / "types"
179+
180+ script_parts = [HEADER ]
181+
182+ validators_by_type = {}
183+
184+ for type in sorted (types_dir .glob ("*.json" )):
185+ type_def = json .loads (type .read_text ())
186+
187+ _type = type_def ["type" ]
188+ standard_validator_class = "TypeValidator"
189+
190+ class_prefix = _type .capitalize ()
191+ class_name = f"{ class_prefix } { standard_validator_class } "
192+ validators_by_type [_type ] = class_name
193+ name_normalization_rules = type_def ["name_definition" ].get ("normalization_rules" ) or []
194+ allowed_qualifiers = [defintion .get ("key" ) for defintion in type_def .get ("qualifiers_definition" ) or []]
195+ namespace_case_sensitive = type_def ["namespace_definition" ].get ("case_sensitive" ) or False
196+ name_case_sensitive = type_def ["name_definition" ].get ("case_sensitive" ) or False
197+ version_definition = type_def .get ("version_definition" ) or {}
198+ version_case_sensitive = version_definition .get ("case_sensitive" ) or True
199+ repository = type_def .get ("repository" )
200+ use_repository_url = repository .get ("use_repository" ) or False
201+
202+ if use_repository_url and "repsitory_url" not in allowed_qualifiers :
203+ allowed_qualifiers .append ("repository_url" )
204+
205+ allowed_qualifiers = set (allowed_qualifiers )
206+
207+ type_validator = TEMPLATE .format (** dict (
208+ class_name = class_name ,
209+ validator_class = standard_validator_class ,
210+ type = _type ,
211+ type_name = type_def ["type_name" ],
212+ description = type_def ["description" ],
213+ use_repository = type_def ["repository" ]["use_repository" ],
214+ default_repository_url = type_def ["repository" ].get ("default_repository_url" ) or "" ,
215+ namespace_requirement = type_def ["namespace_definition" ]["requirement" ],
216+ name_normalization_rules = name_normalization_rules ,
217+ allowed_qualifiers = allowed_qualifiers or [],
218+ namespace_case_sensitive = namespace_case_sensitive ,
219+ name_case_sensitive = name_case_sensitive ,
220+ version_case_sensitive = version_case_sensitive ,
221+ purl_pattern = f"pkg:{ _type } /.*"
222+ ))
223+
224+ script_parts .append (type_validator )
225+
226+ script_parts .append (generate_validators_by_type (validators_by_type = validators_by_type ))
227+ # script_parts.append(attach_router(validators_by_type.values()))
228+
229+ validate_script = base_dir / "src" / "packageurl" / "validate.py"
230+
231+ validate_script .write_text ("\n " .join (script_parts ))
232+
233+
234+ def generate_validators_by_type (validators_by_type ):
235+ """
236+ Return a python snippet that maps a type to it's TypeValidator class
237+ """
238+ snippets = []
239+ for type , class_name in validators_by_type .items ():
240+ snippet = f" { type !r} : { class_name } ,"
241+ snippets .append (snippet )
242+
243+ snippets = "\n " .join (snippets )
244+ start = "VALIDATORS_BY_TYPE = {"
245+ end = "}"
246+ return f"{ start } \n { snippets } \n { end } "
247+
248+ def attach_router (classes ):
249+ snippets = []
250+ for class_name in classes :
251+ snippet = f" { class_name } ,"
252+ snippets .append (snippet )
253+ snippets = "\n " .join (snippets )
254+ start = "PACKAGE_REGISTRY = [ \n "
255+ end = "\n ]"
256+ classes = f"{ start } { snippets } { end } "
257+ router_code = '''
258+ validate_router = Router()
259+
260+ for pkg_class in PACKAGE_REGISTRY:
261+ validate_router.append(pattern=pkg_class.purl_pattern, endpoint=pkg_class.validate)
262+ '''
263+ return f"{ classes } { router_code } "
264+
265+
266+ if __name__ == "__main__" :
267+ generate_validators ()
0 commit comments