Skip to content

Commit 80cde20

Browse files
authored
🔨 Maintenance: Exclude api folder from Codecov, clean up unused utils, and improve web-server test coverage (#8050)
1 parent 3111358 commit 80cde20

File tree

8 files changed

+113
-98
lines changed

8 files changed

+113
-98
lines changed

.codecov.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ component_management:
2525
branches:
2626
- "!master"
2727
individual_components:
28-
- component_id: api
29-
paths:
30-
- api/**
3128
- component_id: pkg_aws_library
3229
paths:
3330
- packages/aws-library/**
@@ -133,6 +130,7 @@ comment:
133130

134131

135132
ignore:
133+
- "api/tests"
136134
- "test_*.py"
137135
- "**/generated_models/*.py"
138136
- "**/generated_code/*.py"

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.70.0
1+
0.71.0

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.70.0
2+
current_version = 0.71.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: simcore-service-webserver
44
description: Main service with an interface (http-API & websockets) to the web front-end
5-
version: 0.70.0
5+
version: 0.71.0
66
servers:
77
- url: ''
88
description: webserver
@@ -9951,21 +9951,18 @@ components:
99519951
test_name:
99529952
type: string
99539953
title: Test Name
9954-
error_type:
9955-
type: string
9956-
title: Error Type
9957-
error_message:
9958-
type: string
9959-
title: Error Message
9960-
traceback:
9954+
error_code:
9955+
anyOf:
9956+
- type: string
9957+
- type: 'null'
9958+
title: Error Code
9959+
user_message:
99619960
type: string
9962-
title: Traceback
9961+
title: User Message
9962+
default: Email test failed
99639963
type: object
99649964
required:
99659965
- test_name
9966-
- error_type
9967-
- error_message
9968-
- traceback
99699966
title: EmailTestFailed
99709967
EmailTestPassed:
99719968
properties:

services/web/server/src/simcore_service_webserver/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def _setup_app_from_settings(
5454

5555

5656
async def app_factory() -> web.Application:
57-
"""Created to launch app from gunicorn (see docker/boot.sh)"""
57+
"""WARNING: this is called in the entrypoint of the service. DO NOT CHAGE THE NAME!
58+
59+
Created to launch app from gunicorn (see docker/boot.sh)
60+
"""
5861
from .application import create_application_auth
5962
from .log import setup_logging
6063

services/web/server/src/simcore_service_webserver/email/_handlers.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,18 @@
55
from models_library.emails import LowerCaseEmailStr
66
from pydantic import BaseModel, Field
77
from servicelib.aiohttp.requests_validation import parse_request_body_as
8+
from servicelib.logging_errors import create_troubleshootting_log_kwargs
89

910
from .._meta import API_VTAG
1011
from ..login.decorators import login_required
1112
from ..products import products_web
1213
from ..products.models import Product
1314
from ..security.decorators import permission_required
14-
from ..utils import get_traceback_string
1515
from ..utils_aiohttp import envelope_json_response
1616
from ._core import check_email_server_responsiveness, send_email_from_template
1717
from .settings import get_plugin_settings
1818

19-
logger = logging.getLogger(__name__)
20-
21-
22-
#
23-
# API schema models
24-
#
19+
_logger = logging.getLogger(__name__)
2520

2621

2722
class TestEmail(BaseModel):
@@ -39,28 +34,15 @@ class TestEmail(BaseModel):
3934

4035
class EmailTestFailed(BaseModel):
4136
test_name: str
42-
error_type: str
43-
error_message: str
44-
traceback: str
45-
46-
@classmethod
47-
def create_from_exception(cls, error: Exception, test_name: str):
48-
return cls(
49-
test_name=test_name,
50-
error_type=f"{type(error)}",
51-
error_message=f"{error}",
52-
traceback=get_traceback_string(error),
53-
)
37+
error_code: str | None = None
38+
user_message: str = "Email test failed"
5439

5540

5641
class EmailTestPassed(BaseModel):
5742
fixtures: dict[str, Any]
5843
info: dict[str, Any]
5944

6045

61-
#
62-
# API routes
63-
#
6446
routes = web.RouteTableDef()
6547

6648

@@ -109,10 +91,24 @@ async def test_email(request: web.Request):
10991
)
11092

11193
except Exception as err: # pylint: disable=broad-except
112-
logger.exception(
113-
"test_email failed for %s",
114-
f"{settings.model_dump_json(indent=1)}",
94+
95+
_logger.exception(
96+
**create_troubleshootting_log_kwargs(
97+
user_error_msg="Email test failed",
98+
error=err,
99+
error_context={
100+
"template_name": body.template_name,
101+
"to": body.to,
102+
"from_": body.from_ or product.support_email,
103+
"settings": settings.model_dump(),
104+
},
105+
tip="Check SMTP settings and network connectivity",
106+
)
115107
)
116108
return envelope_json_response(
117-
EmailTestFailed.create_from_exception(error=err, test_name="test_email")
109+
EmailTestFailed(
110+
test_name="test_email",
111+
error_code=getattr(err, "error_code", None),
112+
user_message="Email test failed. Please check the logs for more details.",
113+
)
118114
)

services/web/server/src/simcore_service_webserver/utils.py

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,18 @@
33
"""
44

55
import asyncio
6-
import hashlib
76
import logging
87
import os
9-
import sys
10-
import traceback
118
import tracemalloc
129
from datetime import datetime
13-
from pathlib import Path
1410

1511
from common_library.error_codes import ErrorCodeStr
1612
from typing_extensions import ( # https://docs.pydantic.dev/latest/api/standard_library_types/#typeddict
1713
TypedDict,
1814
)
1915

20-
_CURRENT_DIR = (
21-
Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
22-
)
2316
_logger = logging.getLogger(__name__)
2417

25-
26-
def is_osparc_repo_dir(path: Path) -> bool:
27-
return all(
28-
any(path.glob(expression)) for expression in [".github", "packages", "services"]
29-
)
30-
31-
32-
def search_osparc_repo_dir(max_iter=8):
33-
"""Returns path to root repo dir or None
34-
35-
NOTE: assumes this file within repo, i.e. only happens in edit mode!
36-
"""
37-
root_dir = _CURRENT_DIR
38-
if "services/web/server" in str(root_dir):
39-
it = 1
40-
while not is_osparc_repo_dir(root_dir) and it < max_iter:
41-
root_dir = root_dir.parent
42-
it += 1
43-
44-
if is_osparc_repo_dir(root_dir):
45-
return root_dir
46-
return None
47-
48-
49-
def gravatar_hash(email: str) -> str:
50-
return hashlib.md5(email.lower().encode("utf-8")).hexdigest() # nosec
51-
52-
53-
# -----------------------------------------------
54-
#
55-
# DATE/TIME
56-
#
57-
#
58-
5918
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
6019

6120
SECOND: int = 1
@@ -69,8 +28,6 @@ def now() -> datetime:
6928

7029

7130
def format_datetime(snapshot: datetime) -> str:
72-
# TODO: this fullfills datetime schema!!!
73-
7431
# FIXME: ensure snapshot is ZULU time!
7532
return "{}Z".format(snapshot.isoformat(timespec="milliseconds"))
7633

@@ -108,7 +65,7 @@ class TaskInfoDict(TypedDict):
10865

10966
def get_task_info(task: asyncio.Task) -> TaskInfoDict:
11067
def _format_frame(f):
111-
return StackInfoDict(f_code=f.f_code, f_lineno=f.f_lineno)
68+
return StackInfoDict(f_code=str(f.f_code), f_lineno=str(f.f_lineno))
11269

11370
info = TaskInfoDict(
11471
txt=str(task),
@@ -163,13 +120,3 @@ def compose_support_error_msg(
163120
)
164121

165122
return ". ".join(sentences)
166-
167-
168-
# -----------------------------------------------
169-
#
170-
# FORMATTING
171-
#
172-
173-
174-
def get_traceback_string(exception: BaseException) -> str:
175-
return "".join(traceback.format_exception(exception))

services/web/server/tests/unit/isolated/test_utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import asyncio
2+
import contextlib
13
import time
24
import urllib.parse
35
from datetime import datetime
46

57
from simcore_service_webserver.utils import (
68
DATETIME_FORMAT,
79
compose_support_error_msg,
10+
get_task_info,
811
now_str,
912
to_datetime,
1013
)
@@ -70,3 +73,74 @@ def test_compose_support_error_msg():
7073
msg == "First sentence for Mr.X. Second sentence."
7174
" For more information please forward this message to [email protected] (supportID=OEC:139641204989600)"
7275
)
76+
77+
78+
async def test_get_task_info():
79+
"""Test get_task_info function with asyncio tasks"""
80+
81+
async def dummy_task():
82+
await asyncio.sleep(0.1)
83+
return "task_result"
84+
85+
# Create a named task
86+
task = asyncio.create_task(dummy_task(), name="test_task")
87+
88+
task_info = get_task_info(task)
89+
90+
# Check that task_info is a dictionary
91+
assert isinstance(task_info, dict)
92+
93+
# Check that it contains expected keys from TaskInfoDict
94+
expected_keys = {"txt", "type", "done", "cancelled", "stack", "exception"}
95+
assert all(key in task_info for key in expected_keys)
96+
97+
# Check basic types
98+
assert isinstance(task_info["txt"], str)
99+
assert isinstance(task_info["type"], str)
100+
assert isinstance(task_info["done"], bool)
101+
assert isinstance(task_info["cancelled"], bool)
102+
assert isinstance(task_info["stack"], list)
103+
104+
# Check that task name is in the txt representation
105+
assert "test_task" in task_info["txt"]
106+
107+
# Check that stack contains frame info when task is running
108+
if not task_info["done"]:
109+
assert len(task_info["stack"]) > 0
110+
# Check stack frame structure
111+
for frame_info in task_info["stack"]:
112+
assert "f_code" in frame_info
113+
assert "f_lineno" in frame_info
114+
assert isinstance(frame_info["f_code"], str)
115+
assert isinstance(frame_info["f_lineno"], str)
116+
117+
# Clean up
118+
task.cancel()
119+
with contextlib.suppress(asyncio.CancelledError):
120+
await task
121+
122+
123+
async def test_get_task_info_unnamed_task():
124+
"""Test get_task_info function with unnamed tasks"""
125+
126+
async def dummy_task():
127+
await asyncio.sleep(0.1)
128+
129+
# Create an unnamed task
130+
task = asyncio.create_task(dummy_task())
131+
132+
task_info = get_task_info(task)
133+
134+
# Check basic structure
135+
assert isinstance(task_info, dict)
136+
expected_keys = {"txt", "type", "done", "cancelled", "stack", "exception"}
137+
assert all(key in task_info for key in expected_keys)
138+
139+
# Check that txt contains task representation
140+
assert isinstance(task_info["txt"], str)
141+
assert "Task" in task_info["txt"]
142+
143+
# Clean up
144+
task.cancel()
145+
with contextlib.suppress(asyncio.CancelledError):
146+
await task

0 commit comments

Comments
 (0)