Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Datcore-adapter refactoring #7270

Merged

Conversation

sanderegg
Copy link
Member

@sanderegg sanderegg commented Feb 25, 2025

What do these changes do?

required by #7200

Related issue/s

How to test

Dev-ops checklist

Copy link
Member

@odeimaiz odeimaiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Copy link

codecov bot commented Feb 25, 2025

Codecov Report

Attention: Patch coverage is 98.49624% with 2 lines in your changes missing coverage. Please review.

Project coverage is 85.31%. Comparing base (603a27d) to head (2521650).
Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7270      +/-   ##
==========================================
+ Coverage   85.25%   85.31%   +0.06%     
==========================================
  Files        1684     1674      -10     
  Lines       65237    65055     -182     
  Branches     1106     1106              
==========================================
- Hits        55618    55502     -116     
+ Misses       9305     9239      -66     
  Partials      314      314              
Flag Coverage Δ
integrationtests 65.44% <ø> (+0.43%) ⬆️
unittests 84.34% <98.49%> (+0.05%) ⬆️
Components Coverage Δ
api ∅ <ø> (∅)
pkg_aws_library 94.17% <ø> (ø)
pkg_dask_task_models_library 97.09% <ø> (ø)
pkg_models_library 91.61% <100.00%> (+0.05%) ⬆️
pkg_notifications_library 84.57% <ø> (ø)
pkg_postgres_database 88.28% <ø> (ø)
pkg_service_integration 70.03% <ø> (ø)
pkg_service_library 72.57% <0.00%> (-0.03%) ⬇️
pkg_settings_library 90.61% <ø> (ø)
pkg_simcore_sdk 85.46% <ø> (ø)
agent 96.46% <ø> (ø)
api_server 90.56% <ø> (ø)
autoscaling 96.08% <ø> (ø)
catalog 91.73% <ø> (ø)
clusters_keeper 99.24% <ø> (ø)
dask_sidecar 91.25% <ø> (ø)
datcore_adapter 98.06% <100.00%> (+4.87%) ⬆️
director 76.68% <ø> (+0.09%) ⬆️
director_v2 91.30% <ø> (ø)
dynamic_scheduler 97.33% <ø> (ø)
dynamic_sidecar 89.74% <ø> (ø)
efs_guardian 90.25% <ø> (ø)
invitations 93.28% <ø> (ø)
osparc_gateway_server ∅ <ø> (∅)
payments 92.66% <ø> (ø)
resource_usage_tracker 88.97% <ø> (ø)
storage 86.87% <ø> (+0.11%) ⬆️
webclient ∅ <ø> (∅)
webserver 78.93% <ø> (-0.04%) ⬇️

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 603a27d...2521650. Read the comment docs.

@sanderegg sanderegg force-pushed the storage/datcore-adapter-refactoring branch from b1ccf52 to 2521650 Compare February 25, 2025 10:08
Copy link
Contributor

@matusdrobuliak66 matusdrobuliak66 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks 👍

Copy link
Member

@pcrespov pcrespov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx. Left some suggestions

created_at: Annotated[datetime.datetime, Field(alias="createdAt")]
updated_at: Annotated[datetime.datetime, Field(alias="updatedAt")]

def to_api_model(self) -> PackageMetaData:
Copy link
Member

@pcrespov pcrespov Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We started removing this dependency such that domain models know nothing about schemas, but rather the other way around.

Check from_domain_model and to_domain_model in schemas in https://github.com/itisfoundation/osparc-simcore/blob/a9c4d5450d916851086772705ba9d6c8883b0240/packages/models-library/src/models_library/api_schemas_webserver/groups.py

I also curated some details and an example with chatty for convenience that adapts to clarify :

Both Domain Models and Schemas are implemented with Pydantic, but they serve different purposes and should be kept separate.

Aspect Domain Model (Service Layer) Schema (API Layer)
Purpose Represents business entities Validates API request/response data
Used In Service Layer (services/) Controller Layer (schemas/)
Should Contain? Core business fields & logic API-specific fields (e.g., validation rules, optional fields)
Why Separate? Keeps business logic independent of transport layer (REST, RPC) Ensures API input/output correctness/backwards compatibility and decoupling

Example

books/
├── books_rest.py        # REST Controller
├── books_service.py     # Service Layer
├── books_repository.py  # Repository Layer (Database Access)
├── books_models.py      # Database Models (SQLAlchemy)
├── books_domain.py      # Domain Models (Business Entities)
├── books_schemas.py     # API Request/Response Schemas (with adapter method)

1️⃣ Domain Model (Service Layer)

📌 Stored in books_domain.py

from pydantic import BaseModel

class BookModel(BaseModel):
    """Represents a Book entity inside the business logic."""
    id: int
    title: str
    author: str
    year: int

Used in the Service Layer
Independent of database and API schemas


2️⃣ Schema (API Layer with from_domain_model Method)

📌 Stored in books_schemas.py

from pydantic import BaseModel, Field
from books_domain import BookModel

class BookCreate(InputSchema):
    """Validates incoming API request to create a book."""
    title: str = Field(..., min_length=3, max_length=255)
    author: str = Field(..., min_length=3, max_length=100)
    year: int = Field(..., ge=1000, le=2100)

    def to_domain_model(self) -> BookModel:
         # ...

class BookResponse(OutputSchema):
    """Defines the API response format."""
    id: int
    title: str
    author: str
    year: int

    @classmethod
    def from_domain_model(cls, book: BookModel) -> Self:
        """Converts a domain model to an API response schema."""
        return cls(**book.dict())

Encapsulates conversion logic inside BookResponse
Keeps the transformation method tied to the API Schema
Avoids scattered adapter functions


3️⃣ Service Layer Uses Domain Model

📌 Stored in books_service.py

from books_domain import BookModel
from books_repository import BookRepository

class BookService:
    def __init__(self):
        self.repository = BookRepository()

    async def create_book(self, book_data: dict) -> BookModel:
        """Handles business logic before saving a book."""
        book_id = await self.repository.save_book(book_data)
        return BookModel(id=book_id, **book_data)  # ✅ Returns a Domain Model

Service layer returns domain models (not API schemas)


4️⃣ Controller Uses Schema’s from_domain_model() Method

📌 Stored in books_rest.py

from fastapi import APIRouter, HTTPException
from books_service import BookService
from books_schemas import BookCreate, BookResponse

router = APIRouter()
book_service = BookService()

@router.post("/books/", response_model=BookResponse)
async def create_book(book: BookCreate):
    """Handles book creation request."""
        created_book = await book_service.create_book(book.to_domain_model())
        return BookResponse.from_domain_model(created_book)  # ✅ Uses schema method

Uses BookCreate for input validation
Uses book.to_domain_model() for conversion
Uses BookResponse.from_domain_model() for conversion
Makes transformation an intuitive part of the schema


Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"We started removing this dependency such that domain models know nothing about schemas, but rather the other way around."
so your api models (living in models-library) know about the models inside the service? that does not sound right at all. are you sure?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, as discussed already many times this the way to go. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer having a separate utils model in that case

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pcrespov that means all your models are living in models-library? doesn't seem right.

Copy link
Member

@pcrespov pcrespov Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"We started removing this dependency such that domain models know nothing about schemas, but rather the other way around." so your api models (living in models-library) know about the models inside the service? that does not sound right at all. are you sure?

@pcrespov that means all your models are living in models-library? doesn't seem right.
No, that is not the case.

@sanderegg let me elaborate a bit more

The models_library has

  • schemas (under api_schema_*)
  • domain models

Then

    1. there, domain models can be imported in schemas but not the other way around.
    1. but, when you have a domain model that lives in your service (e.g. models/domains) then you create the adapters from_domain_model/to_domain_model as free function in your own service. There is no need to move it to models_library

The point 2 does not happen a lot in the webserver because most of the domain models are already in the models_library (that is why i did not mention before)

An alternative would be that all adapters live in a separate place i.e.

books/
├── books_rest.py        # REST Controller
├── books_service.py     # Service Layer
├── books_repository.py  # Repository Layer (Database Access)
├── books_models.py      # Database Models (SQLAlchemy)
├── books_domain.py      # Domain Models (Business Entities)
├── books_schemas.py     # API Request/Response Schemas
├── books_adapter.py     # 📌 Converts Domain Model → API Schema

@sanderegg sanderegg merged commit 731dd9a into ITISFoundation:master Feb 25, 2025
93 of 95 checks passed
@sanderegg sanderegg deleted the storage/datcore-adapter-refactoring branch February 25, 2025 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants