Skip to content

Commit 4a02781

Browse files
fix a url generation error for default api helper, improve flexibility of the base consysapi object by subclassing based on request type
1 parent b114f9a commit 4a02781

6 files changed

Lines changed: 1204 additions & 34 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "oshconnect"
3-
version = "0.5.1a3"
3+
version = "0.5.1a5"
44
description = "Library for interfacing with OSH, helping guide visualization efforts, and providing a place to store configurations. Implements OGC CS API Part 3 (Pub/Sub) MQTT topic conventions including :data topics and resource event topics."
55
readme = "README.md"
66
authors = [

src/oshconnect/csapi4py/con_sys_api.py

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,106 @@
66
from .request_wrappers import post_request, put_request, get_request, delete_request
77

88

9+
class APIRequest(BaseModel):
10+
"""Base for per-verb request classes.
11+
12+
Holds the fields every HTTP method shares: ``url`` (required),
13+
``headers``, ``auth``. Subclasses (`GetRequest`, `PostRequest`,
14+
`PutRequest`, `DeleteRequest`) extend with verb-specific fields —
15+
``params`` for GET/DELETE, ``body`` for POST/PUT — so the type
16+
system rejects incoherent shapes (e.g. a GET carrying a body) at
17+
construction time instead of silently sending them.
18+
19+
Subclasses implement ``execute()`` to dispatch through the
20+
matching ``request_wrappers`` function.
21+
"""
22+
url: HttpUrl = Field(...)
23+
headers: Union[dict, None] = Field(None)
24+
auth: Union[tuple, None] = Field(None)
25+
26+
def execute(self):
27+
raise NotImplementedError("APIRequest subclasses must implement execute().")
28+
29+
30+
class GetRequest(APIRequest):
31+
"""GET — query parameters only; no body."""
32+
params: Union[dict, None] = Field(None)
33+
34+
def execute(self):
35+
return get_request(self.url, self.params, self.headers, self.auth)
36+
37+
38+
class PostRequest(APIRequest):
39+
"""POST — body, optional. ``dict`` lands in ``json``, ``str`` in ``data``."""
40+
body: Union[dict, str, None] = Field(None)
41+
42+
def execute(self):
43+
return post_request(self.url, self.body, self.headers, self.auth)
44+
45+
46+
class PutRequest(APIRequest):
47+
"""PUT — body, optional. Same body routing as POST."""
48+
body: Union[dict, str, None] = Field(None)
49+
50+
def execute(self):
51+
return put_request(self.url, self.body, self.headers, self.auth)
52+
53+
54+
class DeleteRequest(APIRequest):
55+
"""DELETE — query parameters only. HTTP allows a body but the
56+
project's wrapper doesn't pass one, so we don't model it here."""
57+
params: Union[dict, None] = Field(None)
58+
59+
def execute(self):
60+
return delete_request(self.url, self.params, self.headers, self.auth)
61+
62+
963
class ConnectedSystemAPIRequest(BaseModel):
10-
url: HttpUrl = Field(None)
11-
body: Union[dict, str] = Field(None)
12-
params: dict = Field(None)
64+
"""Legacy single-class request shape used by the fluent
65+
``ConnectedSystemsRequestBuilder`` and the free helper functions
66+
in ``oshconnect.api_helpers``. New code in ``APIHelper`` uses the
67+
per-verb subclasses above.
68+
"""
69+
url: Union[HttpUrl, None] = Field(None)
70+
body: Union[dict, str, None] = Field(None)
71+
params: Union[dict, None] = Field(None)
1372
request_method: str = Field('GET')
14-
headers: dict = Field(None)
73+
headers: Union[dict, None] = Field(None)
1574
auth: Union[tuple, None] = Field(None)
1675

1776
def make_request(self):
77+
self._validate_for_send()
1878
match self.request_method:
1979
case 'GET':
2080
return get_request(self.url, self.params, self.headers, self.auth)
2181
case 'POST':
22-
print(f'POST request: {self}')
2382
return post_request(self.url, self.body, self.headers, self.auth)
2483
case 'PUT':
25-
print(f'PUT request: {self}')
2684
return put_request(self.url, self.body, self.headers, self.auth)
2785
case 'DELETE':
28-
print(f'DELETE request: {self}')
2986
return delete_request(self.url, self.params, self.headers, self.auth)
3087
case _:
31-
raise ValueError('Invalid request method')
88+
raise ValueError(f'Invalid request method: {self.request_method!r}')
89+
90+
def _validate_for_send(self):
91+
"""Final coherence check before dispatch.
92+
93+
``url`` may be ``None`` during builder-style construction, but
94+
an unset URL at send time is a programming error. ``GET`` with
95+
a body is well-formed at the HTTP level but most servers ignore
96+
the body — we reject it so the caller doesn't silently send
97+
data that goes nowhere. ``POST``/``PUT`` bodies are optional;
98+
``DELETE`` with a body is allowed by HTTP and accepted here.
99+
"""
100+
if self.url is None:
101+
raise ValueError(
102+
"ConnectedSystemAPIRequest cannot be sent: 'url' is not set."
103+
)
104+
if self.request_method == 'GET' and self.body is not None:
105+
raise ValueError(
106+
"GET requests must not carry a body; pass query parameters "
107+
"via 'params' instead."
108+
)
32109

33110

34111
class ConnectedSystemsRequestBuilder(BaseModel):

src/oshconnect/csapi4py/default_api_helpers.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from pydantic import BaseModel, Field
1414

15-
from .con_sys_api import ConnectedSystemAPIRequest
15+
from .con_sys_api import DeleteRequest, GetRequest, PostRequest, PutRequest
1616
from .constants import APIResourceTypes, ContentTypes, APITerms
1717

1818

@@ -122,10 +122,9 @@ def create_resource(self, res_type: APIResourceTypes, json_data: any, parent_res
122122
if url_endpoint is None:
123123
url = self.resource_url_resolver(res_type, None, parent_res_id, from_collection)
124124
else:
125-
url = f'{self.server_url}/{self.api_root}/{url_endpoint}'
126-
api_request = ConnectedSystemAPIRequest(url=url, request_method='POST', auth=self.get_helper_auth(),
127-
body=json_data, headers=req_headers)
128-
return api_request.make_request()
125+
url = f'{self.get_api_root_url()}/{url_endpoint}'
126+
return PostRequest(url=url, body=json_data, headers=req_headers,
127+
auth=self.get_helper_auth()).execute()
129128

130129
def retrieve_resource(self, res_type: APIResourceTypes, res_id: str = None, parent_res_id: str = None,
131130
from_collection: bool = False,
@@ -145,10 +144,9 @@ def retrieve_resource(self, res_type: APIResourceTypes, res_id: str = None, pare
145144
if url_endpoint is None:
146145
url = self.resource_url_resolver(res_type, res_id, parent_res_id, from_collection)
147146
else:
148-
url = f'{self.server_url}/{self.api_root}/{url_endpoint}'
149-
api_request = ConnectedSystemAPIRequest(url=url, request_method='GET', auth=self.get_helper_auth(),
150-
headers=req_headers)
151-
return api_request.make_request()
147+
url = f'{self.get_api_root_url()}/{url_endpoint}'
148+
return GetRequest(url=url, headers=req_headers,
149+
auth=self.get_helper_auth()).execute()
152150

153151
def get_resource(self, resource_type: APIResourceTypes, resource_id: str = None,
154152
subresource_type: APIResourceTypes = None,
@@ -171,11 +169,8 @@ def get_resource(self, resource_type: APIResourceTypes, resource_id: str = None,
171169
res_id_str = f'/{resource_id}' if resource_id else ""
172170
sub_res_type_str = f'/{resource_type_to_endpoint(subresource_type)}' if subresource_type else ""
173171
complete_url = f'{base_api_url}/{resource_type_str}{res_id_str}{sub_res_type_str}'
174-
api_request = ConnectedSystemAPIRequest(url=complete_url, request_method='GET', auth=self.get_helper_auth(),
175-
headers=req_headers)
176-
if params is not None:
177-
api_request.params = params
178-
return api_request.make_request()
172+
return GetRequest(url=complete_url, params=params, headers=req_headers,
173+
auth=self.get_helper_auth()).execute()
179174

180175
def update_resource(self, res_type: APIResourceTypes, res_id: str, json_data: any, parent_res_id: str = None,
181176
from_collection: bool = False, url_endpoint: str = None, req_headers: dict = None):
@@ -192,12 +187,11 @@ def update_resource(self, res_type: APIResourceTypes, res_id: str, json_data: an
192187
:return:
193188
"""
194189
if url_endpoint is None:
195-
url = self.resource_url_resolver(res_type, None, parent_res_id, from_collection)
190+
url = self.resource_url_resolver(res_type, res_id, parent_res_id, from_collection)
196191
else:
197-
url = f'{self.server_url}/{self.api_root}/{url_endpoint}'
198-
api_request = ConnectedSystemAPIRequest(url=url, request_method='PUT', auth=self.get_helper_auth(),
199-
body=json_data, headers=req_headers)
200-
return api_request.make_request()
192+
url = f'{self.get_api_root_url()}/{url_endpoint}'
193+
return PutRequest(url=url, body=json_data, headers=req_headers,
194+
auth=self.get_helper_auth()).execute()
201195

202196
def delete_resource(self, res_type: APIResourceTypes, res_id: str, parent_res_id: str = None,
203197
from_collection: bool = False, url_endpoint: str = None, req_headers: dict = None):
@@ -213,12 +207,11 @@ def delete_resource(self, res_type: APIResourceTypes, res_id: str, parent_res_id
213207
:return:
214208
"""
215209
if url_endpoint is None:
216-
url = self.resource_url_resolver(res_type, None, parent_res_id, from_collection)
210+
url = self.resource_url_resolver(res_type, res_id, parent_res_id, from_collection)
217211
else:
218-
url = f'{self.server_url}/{self.api_root}/{url_endpoint}'
219-
api_request = ConnectedSystemAPIRequest(url=url, request_method='DELETE', auth=self.get_helper_auth(),
220-
headers=req_headers)
221-
return api_request.make_request()
212+
url = f'{self.get_api_root_url()}/{url_endpoint}'
213+
return DeleteRequest(url=url, headers=req_headers,
214+
auth=self.get_helper_auth()).execute()
222215

223216
# Helpers
224217
def resource_url_resolver(self, subresource_type: APIResourceTypes, subresource_id: str = None,

0 commit comments

Comments
 (0)