Coverage for pygeoapi/models/config.py: 60%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 08:15 +0000

1# ================================================================= 

2# 

3# Authors: Sander Schaminee <sander.schaminee@geocat.net> 

4# Francesco Bartoli <xbartolone@gmail.com> 

5# 

6# Copyright (c) 2023 Sander Schaminee 

7# Copyright (c) 2025 Francesco Bartoli 

8# 

9# Permission is hereby granted, free of charge, to any person 

10# obtaining a copy of this software and associated documentation 

11# files (the "Software"), to deal in the Software without 

12# restriction, including without limitation the rights to use, 

13# copy, modify, merge, publish, distribute, sublicense, and/or sell 

14# copies of the Software, and to permit persons to whom the 

15# Software is furnished to do so, subject to the following 

16# conditions: 

17# 

18# The above copyright notice and this permission notice shall be 

19# included in all copies or substantial portions of the Software. 

20# 

21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 

22# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 

23# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 

24# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 

25# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 

26# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 

27# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

28# OTHER DEALINGS IN THE SOFTWARE. 

29# 

30# ================================================================= 

31 

32from pydantic import BaseModel, Field 

33import pydantic 

34 

35# Handle Pydantic v1/v2 compatibility 

36if pydantic.VERSION.startswith('1'): 36 ↛ 41line 36 didn't jump to line 41 because the condition on line 36 was always true

37 model_validator = 'parse_obj' 

38 model_fields = '__fields__' 

39 regex_param = {'regex': r'^\d+\.\d+\..+$'} 

40else: 

41 model_validator = 'model_validate' 

42 model_fields = 'model_fields' 

43 regex_param = {'pattern': r'^\d+\.\d+\..+$'} 

44 

45 

46class APIRules(BaseModel): 

47 """ 

48 Pydantic model for API design rules that must be adhered to. 

49 """ 

50 api_version: str = Field(**regex_param, 

51 description='Semantic API version number.') 

52 url_prefix: str = Field( 

53 '', 

54 description="If set, pygeoapi routes will be prepended with the " 

55 "given URL path prefix (e.g. '/v1'). " 

56 "Defaults to an empty string (no prefix)." 

57 ) 

58 version_header: str = Field( 

59 '', 

60 description="If set, pygeoapi will set a response header with this " 

61 "name and its value will hold the API version. " 

62 "Defaults to an empty string (i.e. no header). " 

63 "Often 'API-Version' or 'X-API-Version' are used here." 

64 ) 

65 strict_slashes: bool = Field( 

66 False, 

67 description="If False (default), URL trailing slashes are allowed. " 

68 "If True, pygeoapi will return a 404." 

69 ) 

70 

71 @staticmethod 

72 def create(**rules_config) -> 'APIRules': 

73 """ 

74 Returns a new APIRules instance for the current API version 

75 and configured rules. 

76 """ 

77 obj = { 

78 k: v for k, v in rules_config.items() 

79 if k in getattr(APIRules, model_fields) 

80 } 

81 # Validation will fail if required `api_version` is missing 

82 # or if `api_version` is not a semantic version number 

83 model_validator_ = getattr(APIRules, model_validator) 

84 return model_validator_(obj) 

85 

86 @property 

87 def response_headers(self) -> dict: 

88 """ 

89 Gets a dictionary of additional response headers for the current 

90 API rules. Returns an empty dict if no rules apply. 

91 """ 

92 headers = {} 

93 if self.version_header: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 headers[self.version_header] = self.api_version 

95 return headers 

96 

97 def get_url_prefix(self, style: str = '') -> str: 

98 """ 

99 Returns an API URL prefix to use in all paths. 

100 May include a (partial) API version. See docs for syntax. 

101 

102 :param style: Set to 'django', 'flask' or 'starlette' to return a 

103 specific prefix formatted for those frameworks. 

104 If not set, only the prefix itself will be returned. 

105 """ 

106 if not self.url_prefix: 106 ↛ 108line 106 didn't jump to line 108 because the condition on line 106 was always true

107 return '' 

108 major, minor, build = self.api_version.split('.') 

109 prefix = self.url_prefix.format( 

110 api_version=self.api_version, 

111 api_major=major, 

112 api_minor=minor, 

113 api_build=build 

114 ).strip('/') 

115 

116 if style == 'django': 

117 # Django requires the slash at the end 

118 return rf'^{prefix}/' 

119 elif style in ('flask', 'starlette'): 

120 # Flask and Starlette need the slash in front 

121 return f'/{prefix}' 

122 else: 

123 # If no format is specified, return only the bare prefix 

124 return prefix