Skip to content

Commit ff0cf0c

Browse files
authored
HTTP method parameter (#10)
* HTTP method parameter * Fixing typo in docs * Added e2e test * Black
1 parent 20b3287 commit ff0cf0c

File tree

8 files changed

+146
-14
lines changed

8 files changed

+146
-14
lines changed

daeploy/_service/service.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
get_service_name,
2727
get_service_version,
2828
get_service_root_path,
29+
HTTP_METHODS,
2930
)
3031

3132
setup_logging()
@@ -72,7 +73,11 @@ def get_all_parameters() -> dict:
7273
self.app.get("/~parameters", tags=["Parameters"])(get_all_parameters)
7374

7475
def entrypoint(
75-
self, func: Callable = None, monitor: bool = False, **fastapi_kwargs
76+
self,
77+
func: Callable = None,
78+
method: str = "POST",
79+
monitor: bool = False,
80+
**fastapi_kwargs,
7681
) -> Callable:
7782
"""Registers a function as an entrypoint, which will make it reachable
7883
as an HTTP method on your host machine.
@@ -88,17 +93,26 @@ def my_function(arg1:type1) -> type2:
8893
8994
Args:
9095
func (Callable): The decorated function to make an entrypoint for.
96+
method (str): HTTP method for entrypoint. Defauts to "POST"
9197
monitor (bool): Set if the input and output to this entrypoint should
92-
be saved to the service's monitoring database.
98+
be saved to the service's monitoring database. Defaults to False.
9399
**fastapi_kwargs: Keyword arguments for the resulting API endpoint.
94-
See FastAPI for keyword arguments of the ``FastAPI.post()`` function.
100+
See FastAPI for keyword arguments of the ``FastAPI.api_route()``
101+
function.
95102
96103
Raises:
97104
TypeError: If :obj:`func` is not callable.
105+
ValueError: If method is not a valid HTTP method.
98106
99107
Returns:
100108
Callable: The decorated function: :obj:`func`.
101109
"""
110+
method = method.upper()
111+
if method not in HTTP_METHODS:
112+
raise ValueError(
113+
f"Invalid HTTP method: {method}." f" Possible options: {HTTP_METHODS}"
114+
)
115+
102116
# pylint: disable=protected-access
103117
def entrypoint_decorator(deco_func):
104118
funcname = deco_func.__name__
@@ -149,7 +163,9 @@ async def wrapper(_request: Request, *args, **kwargs):
149163
kwargs.update(fastapi_kwargs)
150164

151165
# Create API endpoint
152-
self.app.post(path, tags=["Entrypoints"], **kwargs)(wrapper)
166+
self.app.api_route(path, methods=[method], tags=["Entrypoints"], **kwargs)(
167+
wrapper
168+
)
153169

154170
# Wrap the original func in a pydantic validation wrapper and return that
155171
return validate_arguments(deco_func)

daeploy/communication.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_service_version,
1313
get_headers,
1414
get_authorized_domains,
15+
HTTP_METHODS,
1516
)
1617

1718
logger = logging.getLogger(__name__)
@@ -163,6 +164,7 @@ def call_service(
163164
entrypoint_name: str,
164165
arguments: dict = None,
165166
service_version: str = None,
167+
entrypoint_method: str = "POST",
166168
**request_kwargs,
167169
) -> Any:
168170
"""Call an entrypoint in a different service.
@@ -181,11 +183,18 @@ def call_service(
181183
The return object(s) of this entrypoint must be jsonable, i.e pass FastAPI's
182184
jsonable_encoder, otherwise it won't be reachable.
183185
arguments (dict): Arguments to the entrypoint.
184-
In the form: {"argument_name": value, ...}. Default None.
186+
In the form: {"argument_name": value, ...}. Defaults to None.
185187
service_version (str): The specific version of the service to call.
186-
Default None, the main version and the shadows versions will be called.
188+
Defaults to None, in which case the main version and the shadows
189+
versions will be called.
190+
entrypoint_method (str): HTTP method of the entrypoint to call. You only need
191+
to change this if you have created an entrypoint with a non-default HTTP
192+
method. Defaults to "POST".
187193
**request_kwargs: Keyword arguments to pass on to :func:``requests.post``.
188194
195+
Raises:
196+
ValueError: If entrypoint_method is not a valid HTTP method
197+
189198
Returns:
190199
Any: The output from the entrypoint in the other service.
191200
"""
@@ -197,6 +206,13 @@ def call_service(
197206
else:
198207
url = f"{get_daeploy_manager_url()}/services/{service_name}/{entrypoint_name}"
199208

209+
entrypoint_method = entrypoint_method.upper()
210+
if entrypoint_method not in HTTP_METHODS:
211+
raise ValueError(
212+
f"Invalid HTTP method: {entrypoint_method}."
213+
f" Possible options: {HTTP_METHODS}"
214+
)
215+
200216
arguments = arguments if arguments else {}
201217

202218
logger_msg = f"Calling entrypoint: {entrypoint_name} in service: {service_name}"
@@ -208,7 +224,7 @@ def call_service(
208224
logger.info(f"Sending POST request to: {url}")
209225

210226
response = request(
211-
"POST",
227+
entrypoint_method,
212228
url=url,
213229
auth_domains=get_authorized_domains(),
214230
headers=get_headers(),

daeploy/utilities.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
UNKNOWN_NAME = "unknown"
88
UNKNOWN_VERSION = "0.0.0"
9+
HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE", "PATCH"]
910

1011

1112
def get_daeploy_manager_url() -> str:

docs/source/content/advanced_tutorials/sdk_typing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ that pydantic supports to find which schema fits you data type.
304304

305305

306306
.. note:: Using :py:class:`~daeploy.data_types.ArrayInput` and
307-
:py:class:`~daeploy.data_types.dataFrameInput` as input type can be likened to using
307+
:py:class:`~daeploy.data_types.DataFrameInput` as input type can be likened to using
308308
``list`` or ``dict`` and doesn't give as much control of the validation as the
309309
pydantic models. So in some cases it might be better to use pydantic and convert
310310
the data inside of the entrypoint.

tests/e2e_test/downstream/downstream.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def raise_notification() -> str:
2727
return "Done"
2828

2929

30+
@service.entrypoint(method="GET")
31+
def get_method() -> str:
32+
return "Get - Got - Gotten"
33+
34+
3035
class model1(BaseModel):
3136
name: str
3237
sirname: str

tests/e2e_test/e2e_test.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,16 @@ def test_reaching_daeploy_entrypoint_with_basemodel_args(
253253
def test_call_service_multiple_cases(
254254
dummy_manager, cli_auth_login, services, logs, headers
255255
):
256-
url = "http://localhost/services/upstream/call_downstream_method"
256+
url = "http://localhost/services/upstream/"
257+
url_downstream_post = url + "call_downstream_method"
258+
url_downstream_get = url + "call_downstream_get_method"
257259

258260
unique_name = str(uuid.uuid1())
259261

260262
# Correct arguments
261263
resp = requests.request(
262264
"POST",
263-
url=url,
265+
url=url_downstream_post,
264266
json={
265267
"service_name": "downstream",
266268
"entrypoint_name": "hello",
@@ -282,7 +284,7 @@ def test_call_service_multiple_cases(
282284
unique_name = str(uuid.UUID)
283285
resp = requests.request(
284286
"POST",
285-
url=url,
287+
url=url_downstream_post,
286288
json={
287289
"service_name": "downstream",
288290
"entrypoint_name": "hello",
@@ -298,7 +300,7 @@ def test_call_service_multiple_cases(
298300
unique_name = str(uuid.uuid1())
299301
resp = requests.request(
300302
"POST",
301-
url=url,
303+
url=url_downstream_post,
302304
json={
303305
"service_name": "downstream",
304306
"entrypoint_name": "hello",
@@ -314,7 +316,7 @@ def test_call_service_multiple_cases(
314316
unique_name = str(uuid.uuid1())
315317
resp = requests.request(
316318
"POST",
317-
url=url,
319+
url=url_downstream_post,
318320
json={
319321
"service_name": "downstream",
320322
"entrypoint_name": "hello_TYPO",
@@ -330,7 +332,7 @@ def test_call_service_multiple_cases(
330332
unique_name = str(uuid.uuid1())
331333
resp = requests.request(
332334
"POST",
333-
url=url,
335+
url=url_downstream_post,
334336
json={
335337
"service_name": "downstream_TYPO",
336338
"entrypoint_name": "hello",
@@ -342,6 +344,19 @@ def test_call_service_multiple_cases(
342344
upstream_logs = logs("upstream")
343345
assert "Not Found" in upstream_logs
344346

347+
# Call get method
348+
resp = requests.request(
349+
"POST",
350+
url=url_downstream_get,
351+
json={
352+
"service_name": "downstream",
353+
},
354+
headers=headers,
355+
)
356+
357+
assert resp.status_code == 200
358+
assert resp.json() == "Get - Got - Gotten"
359+
345360

346361
def test_raised_notification_from_service_ends_up_at_manager(
347362
dummy_manager, cli_auth_login, services, headers

tests/e2e_test/upstream/upstream.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,13 @@ def call_downstream_method(service_name, entrypoint_name, arguments):
2020
return greeting
2121

2222

23+
@service.entrypoint
24+
def call_downstream_get_method(service_name):
25+
response = call_service(
26+
service_name=service_name, entrypoint_name="get_method", entrypoint_method="GET"
27+
)
28+
return response
29+
30+
2331
if __name__ == "__main__":
2432
service.run()

tests/sdk_test/daeploy_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,23 @@ def test_call_service_log_args(request, caplog):
479479
assert f"{arguments}" in caplog.text
480480

481481

482+
@patch("daeploy.communication.request")
483+
def test_call_service_invalid_method(request):
484+
service_name = "myservice"
485+
entrypoint_name = "mymethod"
486+
arguments = {"value": 10, "active": False}
487+
version = "1.0.0"
488+
489+
with pytest.raises(ValueError):
490+
call_service(
491+
service_name=service_name,
492+
entrypoint_name=entrypoint_name,
493+
service_version=version,
494+
arguments=arguments,
495+
entrypoint_method="NOT A HTTP METHOD",
496+
)
497+
498+
482499
def test_local_invocation_pydantic_validation():
483500
service = _Service()
484501
wrapped = service.entrypoint(valid_entrypoint_method_args)
@@ -562,6 +579,60 @@ def test_entrypoint_not_monitored():
562579
service.store.assert_not_called()
563580

564581

582+
def test_entrypoint_get():
583+
service = _Service()
584+
service.entrypoint(monitor=False, method="GET")(valid_entrypoint_method_args)
585+
client = TestClient(service.app)
586+
587+
req = {"name": "Rune", "age": 100}
588+
response = client.get(
589+
"/valid_entrypoint_method_args",
590+
json=req,
591+
headers={"accept": "application/json"},
592+
)
593+
assert response.status_code == 200
594+
595+
596+
def test_entrypoint_invalid_method():
597+
service = _Service()
598+
with pytest.raises(ValueError):
599+
service.entrypoint(monitor=False, method="NOT A HTTP METHOD")(
600+
valid_entrypoint_method_args
601+
)
602+
603+
604+
@patch("daeploy.communication.request")
605+
def test_call_service_get_entrypoint(request):
606+
service_name = "myservice"
607+
entrypoint_name = "mymethod"
608+
version = "1.0.0"
609+
arguments = {"value": 10, "active": False}
610+
611+
call_service(
612+
service_name=service_name,
613+
entrypoint_name=entrypoint_name,
614+
arguments=arguments,
615+
service_version=version,
616+
entrypoint_method="GET",
617+
)
618+
619+
expected_url = f"http://host.docker.internal/services/{service_name}_{version}/{entrypoint_name}"
620+
auth_domains = ["localhost"]
621+
expected_headers = {
622+
"Authorization": "Bearer unknown",
623+
"Content-Type": "application/json",
624+
"Host": "localhost",
625+
}
626+
627+
request.assert_called_with(
628+
"GET",
629+
url=expected_url,
630+
auth_domains=auth_domains,
631+
headers=expected_headers,
632+
json=arguments,
633+
)
634+
635+
565636
def test_call_every_decorator():
566637
service = _Service()
567638

0 commit comments

Comments
 (0)