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
« 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# =================================================================
32from pydantic import BaseModel, Field
33import pydantic
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+\..+$'}
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 )
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)
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
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.
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('/')
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