Skip to content

Commit c19c878

Browse files
author
Andrei Neagu
committed
Merge remote-tracking branch 'upstream/master' into pr-osparc-add-dybamic-services-monitor-dashboard2
2 parents 179ac90 + 931595e commit c19c878

File tree

162 files changed

+7497
-2319
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

162 files changed

+7497
-2319
lines changed

.coveragerc

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,20 @@ parallel = True
77

88
[report]
99
# Regexes for lines to exclude from consideration
10-
exclude_lines =
11-
# Have to re-enable the standard pragma
12-
pragma: no cover
13-
10+
exclude_also =
1411
# Don't complain about missing debug-only code:
1512
def __repr__
1613
if self\.debug
17-
1814
# Don't complain if tests don't hit defensive assertion code:
1915
raise AssertionError
2016
raise NotImplementedError
21-
2217
# Don't complain if non-runnable code isn't run:
2318
if 0:
2419
if __name__ == .__main__.:
2520
if __name__ == __main__.:
21+
class .*\bProtocol\):
2622
# Don't complain about abstract methods, they aren't run:
2723
@(abc\.)?abstract(((class|static)?method)|property)
28-
2924
# Don't complain about type checking
3025
if TYPE_CHECKING:
3126

.env-devel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ LOGIN_2FA_REQUIRED=0
334334
LOGIN_ACCOUNT_DELETION_RETENTION_DAYS=31
335335
LOGIN_REGISTRATION_CONFIRMATION_REQUIRED=0
336336
LOGIN_REGISTRATION_INVITATION_REQUIRED=0
337-
PROJECTS_INACTIVITY_INTERVAL=20
337+
PROJECTS_INACTIVITY_INTERVAL=00:00:20
338338
PROJECTS_TRASH_RETENTION_DAYS=7
339339
PROJECTS_MAX_COPY_SIZE_BYTES=30Gib
340340
PROJECTS_MAX_NUM_RUNNING_DYNAMIC_NODES=5

api/specs/director/schemas/scripts/create_node-meta-schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
from pathlib import Path
99

1010
import jsonref
11+
from common_library.json_serialization import json_dumps
1112
from models_library.services import ServiceMetaDataPublished
1213

1314
CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
1415

1516

1617
if __name__ == "__main__":
1718
with Path.open(CURRENT_DIR.parent / "node-meta-v0.0.1-pydantic.json", "w") as f:
18-
schema = ServiceMetaDataPublished.schema_json()
19+
schema = json_dumps(ServiceMetaDataPublished.model_json_schema())
1920
schema_without_ref = jsonref.loads(schema)
2021

2122
json.dump(schema_without_ref, f, indent=2)

api/specs/director/schemas/scripts/create_project-schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99

1010
import jsonref
11+
from common_library.json_serialization import json_dumps
1112
from models_library.projects import Project
1213

1314
CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
@@ -17,7 +18,7 @@
1718
with Path.open(
1819
CURRENT_DIR.parent / "common/schemas/project-v0.0.1-pydantic.json", "w"
1920
) as f:
20-
schema = Project.schema_json()
21+
schema = json_dumps(Project.model_json_schema())
2122
schema_without_ref = jsonref.loads(schema)
2223

2324
json.dump(schema_without_ref, f, indent=2)

api/specs/web-server/_auth.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from typing import Any
88

9-
from _common import Error, Log
109
from fastapi import APIRouter, status
1110
from models_library.api_schemas_webserver.auth import (
1211
AccountRequestInfo,
@@ -15,6 +14,7 @@
1514
UnregisterCheck,
1615
)
1716
from models_library.generics import Envelope
17+
from models_library.rest_error import EnvelopedError, Log
1818
from pydantic import BaseModel, Field, confloat
1919
from simcore_service_webserver._meta import API_VTAG
2020
from simcore_service_webserver.login._2fa_handlers import Resend2faBody
@@ -75,7 +75,7 @@ async def register(_body: RegisterBody):
7575
"/auth/unregister",
7676
response_model=Envelope[Log],
7777
status_code=status.HTTP_200_OK,
78-
responses={status.HTTP_409_CONFLICT: {"model": Envelope[Error]}},
78+
responses={status.HTTP_409_CONFLICT: {"model": EnvelopedError}},
7979
)
8080
async def unregister_account(_body: UnregisterCheck):
8181
...
@@ -107,7 +107,7 @@ async def phone_confirmation(_body: PhoneConfirmationBody):
107107
responses={
108108
# status.HTTP_503_SERVICE_UNAVAILABLE
109109
status.HTTP_401_UNAUTHORIZED: {
110-
"model": Envelope[Error],
110+
"model": EnvelopedError,
111111
"description": "unauthorized reset due to invalid token code",
112112
}
113113
},
@@ -122,7 +122,7 @@ async def login(_body: LoginBody):
122122
operation_id="auth_login_2fa",
123123
responses={
124124
status.HTTP_401_UNAUTHORIZED: {
125-
"model": Envelope[Error],
125+
"model": EnvelopedError,
126126
"description": "unauthorized reset due to invalid token code",
127127
}
128128
},
@@ -137,7 +137,7 @@ async def login_2fa(_body: LoginTwoFactorAuthBody):
137137
operation_id="auth_resend_2fa_code",
138138
responses={
139139
status.HTTP_401_UNAUTHORIZED: {
140-
"model": Envelope[Error],
140+
"model": EnvelopedError,
141141
"description": "unauthorized reset due to invalid token code",
142142
}
143143
},
@@ -161,7 +161,7 @@ async def logout(_body: LogoutBody):
161161
status_code=status.HTTP_204_NO_CONTENT,
162162
responses={
163163
status.HTTP_401_UNAUTHORIZED: {
164-
"model": Envelope[Error],
164+
"model": EnvelopedError,
165165
"description": "unauthorized reset due to invalid token code",
166166
}
167167
},
@@ -174,7 +174,7 @@ async def check_auth():
174174
"/auth/reset-password",
175175
response_model=Envelope[Log],
176176
operation_id="auth_reset_password",
177-
responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"model": Envelope[Error]}},
177+
responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"model": EnvelopedError}},
178178
)
179179
async def reset_password(_body: ResetPasswordBody):
180180
"""a non logged-in user requests a password reset"""
@@ -186,7 +186,7 @@ async def reset_password(_body: ResetPasswordBody):
186186
operation_id="auth_reset_password_allowed",
187187
responses={
188188
status.HTTP_401_UNAUTHORIZED: {
189-
"model": Envelope[Error],
189+
"model": EnvelopedError,
190190
"description": "unauthorized reset due to invalid token code",
191191
}
192192
},
@@ -201,11 +201,11 @@ async def reset_password_allowed(code: str, _body: ResetPasswordConfirmation):
201201
operation_id="auth_change_email",
202202
responses={
203203
status.HTTP_401_UNAUTHORIZED: {
204-
"model": Envelope[Error],
204+
"model": EnvelopedError,
205205
"description": "unauthorized user. Login required",
206206
},
207207
status.HTTP_503_SERVICE_UNAVAILABLE: {
208-
"model": Envelope[Error],
208+
"model": EnvelopedError,
209209
"description": "unable to send confirmation email",
210210
},
211211
},
@@ -233,15 +233,15 @@ class PasswordCheckSchema(BaseModel):
233233
operation_id="auth_change_password",
234234
responses={
235235
status.HTTP_401_UNAUTHORIZED: {
236-
"model": Envelope[Error],
236+
"model": EnvelopedError,
237237
"description": "unauthorized user. Login required",
238238
},
239239
status.HTTP_409_CONFLICT: {
240-
"model": Envelope[Error],
240+
"model": EnvelopedError,
241241
"description": "mismatch between new and confirmation passwords",
242242
},
243243
status.HTTP_422_UNPROCESSABLE_ENTITY: {
244-
"model": Envelope[Error],
244+
"model": EnvelopedError,
245245
"description": "current password is invalid",
246246
},
247247
},

api/specs/web-server/_common.py

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@
55
import sys
66
from collections.abc import Callable
77
from pathlib import Path
8-
from typing import Annotated, NamedTuple, Optional, Union, get_args, get_origin
8+
from typing import (
9+
Annotated,
10+
Any,
11+
Generic,
12+
NamedTuple,
13+
Optional,
14+
TypeVar,
15+
Union,
16+
get_args,
17+
get_origin,
18+
)
919

1020
from common_library.json_serialization import json_dumps
1121
from common_library.pydantic_fields_extension import get_type
1222
from fastapi import Query
13-
from models_library.basic_types import LogLevel
14-
from pydantic import BaseModel, ConfigDict, Field, Json, create_model
23+
from pydantic import BaseModel, Json, create_model
1524
from pydantic.fields import FieldInfo
1625

1726
CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
@@ -78,43 +87,14 @@ def as_query(model_class: type[BaseModel]) -> type[BaseModel]:
7887
return create_model(new_model_name, **fields)
7988

8089

81-
class Log(BaseModel):
82-
level: LogLevel | None = Field("INFO", description="log level")
83-
message: str = Field(
84-
...,
85-
description="log message. If logger is USER, then it MUST be human readable",
86-
)
87-
logger: str | None = Field(
88-
None, description="name of the logger receiving this message"
89-
)
90-
91-
model_config = ConfigDict(
92-
json_schema_extra={
93-
"example": {
94-
"message": "Hi there, Mr user",
95-
"level": "INFO",
96-
"logger": "user-logger",
97-
}
98-
}
99-
)
100-
90+
ErrorT = TypeVar("ErrorT")
10191

102-
class ErrorItem(BaseModel):
103-
code: str = Field(
104-
...,
105-
description="Typically the name of the exception that produced it otherwise some known error code",
106-
)
107-
message: str = Field(..., description="Error message specific to this item")
108-
resource: str | None = Field(
109-
None, description="API resource affected by this error"
110-
)
111-
field: str | None = Field(None, description="Specific field within the resource")
11292

93+
class EnvelopeE(BaseModel, Generic[ErrorT]):
94+
"""Complementary to models_library.generics.Envelope just for the generators"""
11395

114-
class Error(BaseModel):
115-
logs: list[Log] | None = Field(None, description="log messages")
116-
errors: list[ErrorItem] | None = Field(None, description="errors metadata")
117-
status: int | None = Field(None, description="HTTP error code")
96+
error: ErrorT | None = None
97+
data: Any | None = None
11898

11999

120100
class ParamSpec(NamedTuple):

api/specs/web-server/_folders.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
FolderReplaceBodyParams,
1818
)
1919
from models_library.generics import Envelope
20+
from models_library.rest_error import EnvelopedError
2021
from simcore_service_webserver._meta import API_VTAG
22+
from simcore_service_webserver.folders._exceptions_handlers import _TO_HTTP_ERROR_MAP
2123
from simcore_service_webserver.folders._models import (
2224
FolderSearchQueryParams,
2325
FoldersListQueryParams,
@@ -29,6 +31,9 @@
2931
tags=[
3032
"folders",
3133
],
34+
responses={
35+
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
36+
},
3237
)
3338

3439

api/specs/web-server/_workspaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
WorkspaceReplaceBodyParams,
1818
)
1919
from models_library.generics import Envelope
20+
from models_library.rest_error import EnvelopedError
2021
from simcore_service_webserver._meta import API_VTAG
22+
from simcore_service_webserver.folders._exceptions_handlers import _TO_HTTP_ERROR_MAP
2123
from simcore_service_webserver.workspaces._groups_api import WorkspaceGroupGet
2224
from simcore_service_webserver.workspaces._models import (
2325
WorkspacesGroupsBodyParams,
@@ -31,6 +33,9 @@
3133
tags=[
3234
"workspaces",
3335
],
36+
responses={
37+
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
38+
},
3439
)
3540

3641

packages/common-library/src/common_library/json_serialization.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from uuid import UUID
2424

2525
import orjson
26-
from pydantic import NameEmail, SecretBytes, SecretStr
26+
from pydantic import AnyHttpUrl, AnyUrl, HttpUrl, NameEmail, SecretBytes, SecretStr
2727
from pydantic_core import Url
2828
from pydantic_extra_types.color import Color
2929

@@ -62,6 +62,8 @@ def decimal_encoder(dec_value: Decimal) -> int | float:
6262

6363

6464
ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
65+
AnyHttpUrl: str,
66+
AnyUrl: str,
6567
bytes: lambda o: o.decode(),
6668
Color: str,
6769
datetime.date: isoformat,
@@ -73,6 +75,7 @@ def decimal_encoder(dec_value: Decimal) -> int | float:
7375
frozenset: list,
7476
deque: list,
7577
GeneratorType: list,
78+
HttpUrl: str,
7679
IPv4Address: str,
7780
IPv4Interface: str,
7881
IPv4Network: str,

packages/common-library/tests/test_json_serialization.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
json_loads,
1616
)
1717
from faker import Faker
18-
from pydantic import Field, TypeAdapter
18+
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, HttpUrl, TypeAdapter
1919
from pydantic.json import pydantic_encoder
2020

2121

@@ -95,3 +95,18 @@ def test_compatiblity_with_json_interface(
9595

9696
# NOTE: cannot compare dumps directly because orjson compacts it more
9797
assert json_loads(orjson_dump) == json_loads(json_dump)
98+
99+
100+
def test_serialized_model_with_urls(faker: Faker):
101+
# See: https://github.com/ITISFoundation/osparc-simcore/pull/6852
102+
class M(BaseModel):
103+
any_http_url: AnyHttpUrl
104+
any_url: AnyUrl
105+
http_url: HttpUrl
106+
107+
obj = M(
108+
any_http_url=faker.url(),
109+
any_url=faker.url(),
110+
http_url=faker.url(),
111+
)
112+
json_dumps(obj)

0 commit comments

Comments
 (0)